@vanduo-oss/framework 1.3.4 → 1.3.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.
@@ -1,4 +1,4 @@
1
- /*! Vanduo v1.3.4 | Built: 2026-04-14T21:21:55.517Z | git:73e3db5 | development */
1
+ /*! Vanduo v1.3.7 | Built: 2026-04-18T12:05:32.603Z | git:20b2d08 | 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.3.4" : "0.0.0-dev";
110
+ const VANDUO_VERSION = true ? "1.3.7" : "0.0.0-dev";
111
111
  const Vanduo2 = {
112
112
  version: VANDUO_VERSION,
113
113
  components: {},
@@ -6572,6 +6572,410 @@
6572
6572
  window.VanduoGlassScroll = GlassScroll;
6573
6573
  })();
6574
6574
 
6575
+ // js/components/morph.js
6576
+ (function() {
6577
+ "use strict";
6578
+ const MORPH_DURATION_MS = 750;
6579
+ const Morph = {
6580
+ instances: /* @__PURE__ */ new Map(),
6581
+ init: function() {
6582
+ const elements = document.querySelectorAll(".vd-morph, [data-vd-morph]");
6583
+ elements.forEach(function(el) {
6584
+ if (Morph.instances.has(el)) return;
6585
+ if (el.getAttribute("data-vd-morph") === "manual") return;
6586
+ Morph.initInstance(el);
6587
+ });
6588
+ },
6589
+ initInstance: function(el) {
6590
+ Morph._ensureLayers(el);
6591
+ const cleanup = [];
6592
+ let morphing = false;
6593
+ const handleClick = function(e) {
6594
+ if (morphing) return;
6595
+ Morph._runMorph(el, e, function() {
6596
+ morphing = false;
6597
+ });
6598
+ morphing = true;
6599
+ };
6600
+ el.addEventListener("click", handleClick);
6601
+ cleanup.push(function() {
6602
+ el.removeEventListener("click", handleClick);
6603
+ });
6604
+ this.instances.set(el, { cleanup });
6605
+ },
6606
+ morph: function(el) {
6607
+ if (!el) return;
6608
+ if (!this.instances.has(el)) this.initInstance(el);
6609
+ this._runMorph(el, null, null);
6610
+ },
6611
+ destroy: function(el) {
6612
+ const instance = this.instances.get(el);
6613
+ if (!instance) return;
6614
+ instance.cleanup.forEach(function(fn) {
6615
+ fn();
6616
+ });
6617
+ this.instances.delete(el);
6618
+ },
6619
+ destroyAll: function() {
6620
+ this.instances.forEach(function(_, el) {
6621
+ Morph.destroy(el);
6622
+ });
6623
+ },
6624
+ /* ── Internal helpers ── */
6625
+ _ensureLayers: function(el) {
6626
+ if (!el.querySelector(".vd-morph-wave")) {
6627
+ const wave = document.createElement("span");
6628
+ wave.className = "vd-morph-wave";
6629
+ wave.setAttribute("aria-hidden", "true");
6630
+ el.insertBefore(wave, el.firstChild);
6631
+ }
6632
+ if (!el.querySelector(".vd-morph-shine")) {
6633
+ const shine = document.createElement("span");
6634
+ shine.className = "vd-morph-shine";
6635
+ shine.setAttribute("aria-hidden", "true");
6636
+ const waveEl = el.querySelector(".vd-morph-wave");
6637
+ if (waveEl && waveEl.nextSibling) {
6638
+ el.insertBefore(shine, waveEl.nextSibling);
6639
+ } else {
6640
+ el.insertBefore(shine, el.firstChild);
6641
+ }
6642
+ }
6643
+ },
6644
+ _runMorph: function(el, pointerEvent, onComplete) {
6645
+ const wave = el.querySelector(".vd-morph-wave");
6646
+ if (wave) {
6647
+ const rect = el.getBoundingClientRect();
6648
+ const cx = rect.left + rect.width / 2;
6649
+ const cy = rect.top + rect.height / 2;
6650
+ const px = pointerEvent ? pointerEvent.clientX || cx : cx;
6651
+ const py = pointerEvent ? pointerEvent.clientY || cy : cy;
6652
+ wave.style.left = px - rect.left + "px";
6653
+ wave.style.top = py - rect.top + "px";
6654
+ }
6655
+ el.classList.add("is-morphing");
6656
+ let duration = MORPH_DURATION_MS;
6657
+ const custom = getComputedStyle(el).getPropertyValue("--morph-duration");
6658
+ if (custom) {
6659
+ const parsed = parseFloat(custom);
6660
+ if (!isNaN(parsed)) duration = parsed * (custom.indexOf("ms") !== -1 ? 1 : 1e3);
6661
+ }
6662
+ setTimeout(function() {
6663
+ el.classList.remove("is-morphing");
6664
+ const current = el.querySelector(".vd-morph-current");
6665
+ const next = el.querySelector(".vd-morph-next");
6666
+ if (current && next) {
6667
+ current.classList.remove("vd-morph-current");
6668
+ current.classList.add("vd-morph-next");
6669
+ next.classList.remove("vd-morph-next");
6670
+ next.classList.add("vd-morph-current");
6671
+ }
6672
+ if (typeof onComplete === "function") onComplete();
6673
+ }, duration);
6674
+ }
6675
+ };
6676
+ if (typeof window.Vanduo !== "undefined") {
6677
+ window.Vanduo.register("morph", Morph);
6678
+ }
6679
+ window.VanduoMorph = Morph;
6680
+ })();
6681
+
6682
+ // js/components/expanding-cards.js
6683
+ (function() {
6684
+ "use strict";
6685
+ const ExpandingCards = {
6686
+ instances: /* @__PURE__ */ new Map(),
6687
+ init: function() {
6688
+ document.querySelectorAll(".vd-expanding-cards").forEach(function(el) {
6689
+ if (el.getAttribute("data-vd-expanding-cards") === "manual") return;
6690
+ if (ExpandingCards.instances.has(el)) return;
6691
+ ExpandingCards.initContainer(el);
6692
+ });
6693
+ },
6694
+ initContainer: function(container) {
6695
+ const cleanup = [];
6696
+ const getCards = function() {
6697
+ return Array.prototype.slice.call(container.querySelectorAll(".vd-expanding-card"));
6698
+ };
6699
+ const setActive = function(card) {
6700
+ const cards = getCards();
6701
+ if (!card || cards.indexOf(card) === -1) return;
6702
+ cards.forEach(function(c) {
6703
+ c.classList.toggle("is-active", c === card);
6704
+ });
6705
+ card.focus({ preventScroll: true });
6706
+ };
6707
+ const onClick = function(e) {
6708
+ const t = e.target;
6709
+ const card = t.closest ? t.closest(".vd-expanding-card") : null;
6710
+ if (!card || !container.contains(card)) return;
6711
+ setActive(card);
6712
+ };
6713
+ const onKeydown = function(e) {
6714
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight" && e.key !== "Home" && e.key !== "End") {
6715
+ return;
6716
+ }
6717
+ const cards = getCards().filter(function(c) {
6718
+ return c.offsetParent !== null || c.getClientRects().length > 0;
6719
+ });
6720
+ if (!cards.length) return;
6721
+ const activeEl = document.activeElement;
6722
+ let idx = cards.indexOf(activeEl);
6723
+ if (idx < 0) {
6724
+ idx = cards.findIndex(function(c) {
6725
+ return c.classList.contains("is-active");
6726
+ });
6727
+ }
6728
+ if (idx < 0) idx = 0;
6729
+ if (e.key === "ArrowLeft") {
6730
+ e.preventDefault();
6731
+ setActive(cards[Math.max(0, idx - 1)]);
6732
+ } else if (e.key === "ArrowRight") {
6733
+ e.preventDefault();
6734
+ setActive(cards[Math.min(cards.length - 1, idx + 1)]);
6735
+ } else if (e.key === "Home") {
6736
+ e.preventDefault();
6737
+ setActive(cards[0]);
6738
+ } else if (e.key === "End") {
6739
+ e.preventDefault();
6740
+ setActive(cards[cards.length - 1]);
6741
+ }
6742
+ };
6743
+ container.addEventListener("click", onClick);
6744
+ cleanup.push(function() {
6745
+ container.removeEventListener("click", onClick);
6746
+ });
6747
+ container.addEventListener("keydown", onKeydown);
6748
+ cleanup.push(function() {
6749
+ container.removeEventListener("keydown", onKeydown);
6750
+ });
6751
+ getCards().forEach(function(card) {
6752
+ if (!card.hasAttribute("tabindex")) {
6753
+ card.setAttribute("tabindex", "0");
6754
+ }
6755
+ card.setAttribute("role", "button");
6756
+ if (!card.hasAttribute("aria-pressed")) {
6757
+ card.setAttribute("aria-pressed", card.classList.contains("is-active") ? "true" : "false");
6758
+ }
6759
+ });
6760
+ const syncAria = function() {
6761
+ getCards().forEach(function(card) {
6762
+ card.setAttribute("aria-pressed", card.classList.contains("is-active") ? "true" : "false");
6763
+ });
6764
+ };
6765
+ const mo = new MutationObserver(syncAria);
6766
+ mo.observe(container, { attributes: true, subtree: true, attributeFilter: ["class"] });
6767
+ cleanup.push(function() {
6768
+ mo.disconnect();
6769
+ });
6770
+ syncAria();
6771
+ ExpandingCards.instances.set(container, { cleanup });
6772
+ },
6773
+ destroy: function(container) {
6774
+ const inst = this.instances.get(container);
6775
+ if (!inst) return;
6776
+ inst.cleanup.forEach(function(fn) {
6777
+ fn();
6778
+ });
6779
+ this.instances.delete(container);
6780
+ },
6781
+ destroyAll: function() {
6782
+ this.instances.forEach(function(_, el) {
6783
+ ExpandingCards.destroy(el);
6784
+ });
6785
+ }
6786
+ };
6787
+ if (typeof window.Vanduo !== "undefined") {
6788
+ window.Vanduo.register("expandingCards", ExpandingCards);
6789
+ }
6790
+ window.VanduoExpandingCards = ExpandingCards;
6791
+ })();
6792
+
6793
+ // js/components/timeline.js
6794
+ (function() {
6795
+ "use strict";
6796
+ const STAGGER_MS = 140;
6797
+ const MAX_STAGGER_INDEX = 7;
6798
+ const PLAY_INTERVAL_MS = 800;
6799
+ function countRevealedPrefix(items) {
6800
+ let count = 0;
6801
+ for (let i = 0; i < items.length; i++) {
6802
+ if (!items[i].classList.contains("is-revealed")) break;
6803
+ count++;
6804
+ }
6805
+ return count;
6806
+ }
6807
+ function findPlaybackControls(container) {
6808
+ return container.parentElement || document.body;
6809
+ }
6810
+ function initPlayback(container, items, cleanup) {
6811
+ items.forEach(function(item) {
6812
+ item.classList.remove("is-revealed");
6813
+ });
6814
+ const scope = findPlaybackControls(container);
6815
+ const prevBtn = scope.querySelector("[data-vd-timeline-prev]");
6816
+ const nextBtn = scope.querySelector("[data-vd-timeline-next]");
6817
+ const playBtn = scope.querySelector("[data-vd-timeline-play]");
6818
+ const pauseBtn = scope.querySelector("[data-vd-timeline-pause]");
6819
+ let playTimer = null;
6820
+ function updateNavButtons() {
6821
+ const k = countRevealedPrefix(items);
6822
+ const n = items.length;
6823
+ if (prevBtn) {
6824
+ const atStart = k === 0;
6825
+ prevBtn.disabled = atStart;
6826
+ prevBtn.setAttribute("aria-disabled", atStart ? "true" : "false");
6827
+ }
6828
+ if (nextBtn) {
6829
+ const atEnd = k >= n;
6830
+ nextBtn.disabled = atEnd;
6831
+ nextBtn.setAttribute("aria-disabled", atEnd ? "true" : "false");
6832
+ }
6833
+ if (playBtn) {
6834
+ playBtn.setAttribute("aria-pressed", playTimer ? "true" : "false");
6835
+ }
6836
+ if (pauseBtn) {
6837
+ pauseBtn.disabled = !playTimer;
6838
+ }
6839
+ }
6840
+ function stepNext() {
6841
+ const k = countRevealedPrefix(items);
6842
+ if (k < items.length) {
6843
+ items[k].classList.add("is-revealed");
6844
+ }
6845
+ updateNavButtons();
6846
+ }
6847
+ function stepPrev() {
6848
+ const k = countRevealedPrefix(items);
6849
+ if (k > 0) {
6850
+ items[k - 1].classList.remove("is-revealed");
6851
+ }
6852
+ updateNavButtons();
6853
+ }
6854
+ function play() {
6855
+ if (playTimer) return;
6856
+ playTimer = setInterval(function() {
6857
+ if (countRevealedPrefix(items) >= items.length) {
6858
+ pause();
6859
+ return;
6860
+ }
6861
+ stepNext();
6862
+ }, PLAY_INTERVAL_MS);
6863
+ updateNavButtons();
6864
+ }
6865
+ function pause() {
6866
+ if (playTimer) {
6867
+ clearInterval(playTimer);
6868
+ playTimer = null;
6869
+ }
6870
+ updateNavButtons();
6871
+ }
6872
+ function addClick(el, fn) {
6873
+ if (!el) return;
6874
+ const handler = function(e) {
6875
+ e.preventDefault();
6876
+ fn();
6877
+ };
6878
+ el.addEventListener("click", handler);
6879
+ cleanup.push(function() {
6880
+ el.removeEventListener("click", handler);
6881
+ });
6882
+ }
6883
+ addClick(prevBtn, stepPrev);
6884
+ addClick(nextBtn, stepNext);
6885
+ addClick(playBtn, play);
6886
+ addClick(pauseBtn, pause);
6887
+ cleanup.push(function() {
6888
+ pause();
6889
+ });
6890
+ updateNavButtons();
6891
+ return {
6892
+ stepNext,
6893
+ stepPrev,
6894
+ play,
6895
+ pause
6896
+ };
6897
+ }
6898
+ const Timeline = {
6899
+ instances: /* @__PURE__ */ new Map(),
6900
+ init: function() {
6901
+ document.querySelectorAll(".vd-timeline.vd-timeline-animated").forEach(function(el) {
6902
+ if (Timeline.instances.has(el)) return;
6903
+ Timeline.initInstance(el);
6904
+ });
6905
+ },
6906
+ reinit: function() {
6907
+ Timeline.destroyAll();
6908
+ Timeline.init();
6909
+ },
6910
+ initInstance: function(container) {
6911
+ const cleanup = [];
6912
+ const items = Array.prototype.filter.call(container.children, function(child) {
6913
+ return child.classList && child.classList.contains("vd-timeline-item");
6914
+ });
6915
+ items.forEach(function(item, i) {
6916
+ const idx = Math.min(i, MAX_STAGGER_INDEX);
6917
+ item.style.setProperty("--vd-timeline-reveal-delay", idx * STAGGER_MS + "ms");
6918
+ });
6919
+ const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
6920
+ if (reducedMotion) {
6921
+ items.forEach(function(item) {
6922
+ item.classList.add("is-revealed");
6923
+ });
6924
+ Timeline.instances.set(container, { cleanup });
6925
+ return;
6926
+ }
6927
+ const playback = container.classList && container.classList.contains("vd-timeline-playback");
6928
+ if (playback) {
6929
+ const playbackApi = initPlayback(container, items, cleanup);
6930
+ Timeline.instances.set(container, { cleanup, playback: playbackApi });
6931
+ return;
6932
+ }
6933
+ if (typeof IntersectionObserver === "undefined") {
6934
+ items.forEach(function(item) {
6935
+ item.classList.add("is-revealed");
6936
+ });
6937
+ Timeline.instances.set(container, { cleanup });
6938
+ return;
6939
+ }
6940
+ const observer = new IntersectionObserver(function(entries) {
6941
+ entries.forEach(function(entry) {
6942
+ if (!entry.isIntersecting) return;
6943
+ entry.target.classList.add("is-revealed");
6944
+ observer.unobserve(entry.target);
6945
+ });
6946
+ }, {
6947
+ root: null,
6948
+ rootMargin: "0px 0px -10% 0px",
6949
+ threshold: 0.15
6950
+ });
6951
+ items.forEach(function(item) {
6952
+ observer.observe(item);
6953
+ });
6954
+ cleanup.push(function() {
6955
+ observer.disconnect();
6956
+ });
6957
+ Timeline.instances.set(container, { cleanup });
6958
+ },
6959
+ destroy: function(container) {
6960
+ const inst = this.instances.get(container);
6961
+ if (!inst) return;
6962
+ inst.cleanup.forEach(function(fn) {
6963
+ fn();
6964
+ });
6965
+ this.instances.delete(container);
6966
+ },
6967
+ destroyAll: function() {
6968
+ this.instances.forEach(function(_, el) {
6969
+ Timeline.destroy(el);
6970
+ });
6971
+ }
6972
+ };
6973
+ if (typeof window.Vanduo !== "undefined") {
6974
+ window.Vanduo.register("timeline", Timeline);
6975
+ }
6976
+ window.VanduoTimeline = Timeline;
6977
+ })();
6978
+
6575
6979
  // js/components/flow.js
6576
6980
  (function() {
6577
6981
  "use strict";
@@ -7588,6 +7992,115 @@
7588
7992
  "use strict";
7589
7993
  const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
7590
7994
  const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
7995
+ function escapeRegexChar(c) {
7996
+ return c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7997
+ }
7998
+ function buildParseFormat(format) {
7999
+ let regex = "^";
8000
+ const order = [];
8001
+ let i = 0;
8002
+ while (i < format.length) {
8003
+ const slice = format.slice(i);
8004
+ if (slice.toLowerCase().startsWith("yyyy")) {
8005
+ regex += "(\\d{4})";
8006
+ order.push("y");
8007
+ i += 4;
8008
+ } else if (slice.toLowerCase().startsWith("mm")) {
8009
+ regex += "(\\d{2})";
8010
+ order.push("m");
8011
+ i += 2;
8012
+ } else if (slice.toLowerCase().startsWith("dd")) {
8013
+ regex += "(\\d{2})";
8014
+ order.push("d");
8015
+ i += 2;
8016
+ } else {
8017
+ regex += escapeRegexChar(format[i]);
8018
+ i++;
8019
+ }
8020
+ }
8021
+ regex += "$";
8022
+ return { regex: new RegExp(regex), order };
8023
+ }
8024
+ function parseDateFromFormat(value, format) {
8025
+ if (!value || !format) return null;
8026
+ const { regex, order } = buildParseFormat(format);
8027
+ const m = value.trim().match(regex);
8028
+ if (!m) return null;
8029
+ let y;
8030
+ let mo;
8031
+ let d;
8032
+ let ci = 1;
8033
+ for (let k = 0; k < order.length; k++) {
8034
+ const part = order[k];
8035
+ const v = parseInt(m[ci++], 10);
8036
+ if (Number.isNaN(v)) return null;
8037
+ if (part === "y") y = v;
8038
+ else if (part === "m") mo = v - 1;
8039
+ else if (part === "d") d = v;
8040
+ }
8041
+ if (y === void 0 || mo === void 0 || d === void 0) return null;
8042
+ const dt = new Date(y, mo, d);
8043
+ if (dt.getFullYear() !== y || dt.getMonth() !== mo || dt.getDate() !== d) return null;
8044
+ return dt;
8045
+ }
8046
+ function formatDate(d, format) {
8047
+ const yyyy = String(d.getFullYear());
8048
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
8049
+ const dd = String(d.getDate()).padStart(2, "0");
8050
+ let out = "";
8051
+ let i = 0;
8052
+ while (i < format.length) {
8053
+ const slice = format.slice(i);
8054
+ if (slice.toLowerCase().startsWith("yyyy")) {
8055
+ out += yyyy;
8056
+ i += 4;
8057
+ } else if (slice.toLowerCase().startsWith("mm")) {
8058
+ out += mm;
8059
+ i += 2;
8060
+ } else if (slice.toLowerCase().startsWith("dd")) {
8061
+ out += dd;
8062
+ i += 2;
8063
+ } else {
8064
+ out += format[i];
8065
+ i++;
8066
+ }
8067
+ }
8068
+ return out;
8069
+ }
8070
+ function dateKey(d) {
8071
+ return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
8072
+ }
8073
+ function addDays(d, n) {
8074
+ const x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
8075
+ x.setDate(x.getDate() + n);
8076
+ return x;
8077
+ }
8078
+ function addMonthsClamped(d, n) {
8079
+ return new Date(d.getFullYear(), d.getMonth() + n, d.getDate());
8080
+ }
8081
+ function parseYmdLocal(ymd) {
8082
+ if (!ymd || typeof ymd !== "string") return null;
8083
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd.trim());
8084
+ if (!m) return null;
8085
+ const y = +m[1];
8086
+ const mo = +m[2] - 1;
8087
+ const day = +m[3];
8088
+ const dt = new Date(y, mo, day);
8089
+ if (dt.getFullYear() !== y || dt.getMonth() !== mo || dt.getDate() !== day) return null;
8090
+ return dt;
8091
+ }
8092
+ function startOfWeekSunday(d) {
8093
+ const x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
8094
+ const day = x.getDay();
8095
+ x.setDate(x.getDate() - day);
8096
+ return x;
8097
+ }
8098
+ function endOfWeekSunday(d) {
8099
+ const x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
8100
+ const day = x.getDay();
8101
+ x.setDate(x.getDate() + (6 - day));
8102
+ return x;
8103
+ }
7591
8104
  const Datepicker = {
7592
8105
  instances: /* @__PURE__ */ new Map(),
7593
8106
  init: function() {
@@ -7599,28 +8112,70 @@
7599
8112
  },
7600
8113
  initInstance: function(input) {
7601
8114
  const cleanup = [];
7602
- const format = input.getAttribute("data-vd-datepicker-format") || "yyyy-mm-dd";
8115
+ const format = input.getAttribute("data-vd-datepicker-format") || "YYYY-MM-DD";
7603
8116
  const minStr = input.getAttribute("data-vd-datepicker-min");
7604
8117
  const maxStr = input.getAttribute("data-vd-datepicker-max");
7605
- const minDate = minStr ? new Date(minStr) : null;
7606
- const maxDate = maxStr ? new Date(maxStr) : null;
8118
+ const minDate = minStr ? parseYmdLocal(minStr) : null;
8119
+ const maxDate = maxStr ? parseYmdLocal(maxStr) : null;
7607
8120
  const today = /* @__PURE__ */ new Date();
7608
8121
  let viewYear = today.getFullYear();
7609
8122
  let viewMonth = today.getMonth();
7610
8123
  let selectedDate = null;
7611
8124
  let viewMode = "days";
8125
+ let focusedDate = null;
8126
+ let skipNextFocusOpen = false;
8127
+ const isDisabled = (d) => {
8128
+ if (minDate) {
8129
+ const t = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
8130
+ if (t < minDate.getTime()) return true;
8131
+ }
8132
+ if (maxDate) {
8133
+ const t = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
8134
+ if (t > maxDate.getTime()) return true;
8135
+ }
8136
+ return false;
8137
+ };
8138
+ const ensureMonthInRange = (y, m) => {
8139
+ if (!minDate && !maxDate) return { y, m };
8140
+ const first = new Date(y, m, 1);
8141
+ const last = new Date(y, m + 1, 0);
8142
+ if (minDate && last.getTime() < minDate.getTime()) {
8143
+ return { y: minDate.getFullYear(), m: minDate.getMonth() };
8144
+ }
8145
+ if (maxDate && first.getTime() > maxDate.getTime()) {
8146
+ return { y: maxDate.getFullYear(), m: maxDate.getMonth() };
8147
+ }
8148
+ return { y, m };
8149
+ };
8150
+ const firstSelectableInMonth = (y, m) => {
8151
+ const last = new Date(y, m + 1, 0).getDate();
8152
+ for (let day = 1; day <= last; day++) {
8153
+ const dt = new Date(y, m, day);
8154
+ if (!isDisabled(dt)) return dt;
8155
+ }
8156
+ return new Date(y, m, 1);
8157
+ };
7612
8158
  if (input.value) {
7613
- const parsed = new Date(input.value);
7614
- if (!isNaN(parsed.getTime())) {
8159
+ const trimmed = input.value.trim();
8160
+ let parsed = parseDateFromFormat(trimmed, format);
8161
+ if (!parsed) {
8162
+ const fallback = new Date(trimmed);
8163
+ if (!isNaN(fallback.getTime())) parsed = fallback;
8164
+ }
8165
+ if (parsed) {
7615
8166
  selectedDate = parsed;
7616
8167
  viewYear = parsed.getFullYear();
7617
8168
  viewMonth = parsed.getMonth();
7618
8169
  }
7619
8170
  }
8171
+ const clampedInit = ensureMonthInRange(viewYear, viewMonth);
8172
+ viewYear = clampedInit.y;
8173
+ viewMonth = clampedInit.m;
7620
8174
  const popup = document.createElement("div");
7621
8175
  popup.className = "vd-datepicker-popup";
7622
8176
  popup.setAttribute("role", "dialog");
7623
8177
  popup.setAttribute("aria-label", "Choose date");
8178
+ popup.tabIndex = -1;
7624
8179
  const wrapper = document.createElement("div");
7625
8180
  wrapper.className = "vd-suggest-wrapper";
7626
8181
  wrapper.style.position = "relative";
@@ -7628,18 +8183,80 @@
7628
8183
  input.parentNode.insertBefore(wrapper, input);
7629
8184
  wrapper.appendChild(input);
7630
8185
  wrapper.appendChild(popup);
7631
- const formatDate = (d) => {
7632
- const yyyy = d.getFullYear();
7633
- const mm = String(d.getMonth() + 1).padStart(2, "0");
7634
- const dd = String(d.getDate()).padStart(2, "0");
7635
- return format.replace("yyyy", yyyy).replace("mm", mm).replace("dd", dd);
8186
+ const isSameDay = (a, b) => a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
8187
+ const selectDate = (date) => {
8188
+ selectedDate = date;
8189
+ viewYear = date.getFullYear();
8190
+ viewMonth = date.getMonth();
8191
+ input.value = formatDate(date, format);
8192
+ skipNextFocusOpen = true;
8193
+ close();
8194
+ input.dispatchEvent(new CustomEvent("datepicker:select", {
8195
+ detail: { date, formatted: input.value },
8196
+ bubbles: true
8197
+ }));
8198
+ input.dispatchEvent(new Event("change", { bubbles: true }));
8199
+ input.focus();
7636
8200
  };
7637
- const isDisabled = (d) => {
7638
- if (minDate && d < minDate) return true;
7639
- if (maxDate && d > maxDate) return true;
7640
- return false;
8201
+ const focusFocusedDay = () => {
8202
+ if (viewMode !== "days" || !focusedDate) return;
8203
+ const key = dateKey(focusedDate);
8204
+ const btn = popup.querySelector('[data-vd-date="' + key + '"]');
8205
+ if (btn && !btn.classList.contains("is-outside") && btn.getAttribute("aria-disabled") !== "true") {
8206
+ btn.focus();
8207
+ }
8208
+ };
8209
+ const skipDisabled = (d, stepDir, maxSteps) => {
8210
+ let x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
8211
+ const step = stepDir > 0 ? 1 : -1;
8212
+ for (let i = 0; i < maxSteps; i++) {
8213
+ if (!isDisabled(x)) return x;
8214
+ x = addDays(x, step);
8215
+ }
8216
+ return d;
8217
+ };
8218
+ const createDayBtn = (day, outside, date) => {
8219
+ const btn = document.createElement("button");
8220
+ btn.type = "button";
8221
+ btn.className = "vd-datepicker-day";
8222
+ btn.textContent = day;
8223
+ btn.setAttribute("role", "gridcell");
8224
+ if (outside) {
8225
+ btn.classList.add("is-outside");
8226
+ btn.tabIndex = -1;
8227
+ btn.setAttribute("aria-disabled", "true");
8228
+ return btn;
8229
+ }
8230
+ btn.setAttribute("data-vd-date", dateKey(date));
8231
+ if (date && isSameDay(date, today)) btn.classList.add("is-today");
8232
+ if (date && isSameDay(date, selectedDate)) btn.classList.add("is-selected");
8233
+ if (date && isDisabled(date)) {
8234
+ btn.classList.add("is-disabled");
8235
+ btn.setAttribute("aria-disabled", "true");
8236
+ btn.tabIndex = -1;
8237
+ return btn;
8238
+ }
8239
+ if (date) {
8240
+ const isFocused = focusedDate && isSameDay(date, focusedDate);
8241
+ btn.tabIndex = isFocused ? 0 : -1;
8242
+ btn.addEventListener("click", () => {
8243
+ selectedDate = date;
8244
+ viewYear = date.getFullYear();
8245
+ viewMonth = date.getMonth();
8246
+ focusedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
8247
+ input.value = formatDate(date, format);
8248
+ skipNextFocusOpen = true;
8249
+ close();
8250
+ input.dispatchEvent(new CustomEvent("datepicker:select", {
8251
+ detail: { date, formatted: input.value },
8252
+ bubbles: true
8253
+ }));
8254
+ input.dispatchEvent(new Event("change", { bubbles: true }));
8255
+ input.focus();
8256
+ });
8257
+ }
8258
+ return btn;
7641
8259
  };
7642
- const isSameDay = (a, b) => a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
7643
8260
  const render = () => {
7644
8261
  popup.innerHTML = "";
7645
8262
  const header = document.createElement("div");
@@ -7709,35 +8326,53 @@
7709
8326
  header.appendChild(nextBtn);
7710
8327
  popup.appendChild(header);
7711
8328
  if (viewMode === "days") {
8329
+ const gridWrap = document.createElement("div");
8330
+ gridWrap.className = "vd-datepicker-grid";
8331
+ gridWrap.setAttribute("role", "grid");
8332
+ gridWrap.setAttribute("aria-label", "Calendar");
7712
8333
  const weekdays = document.createElement("div");
7713
8334
  weekdays.className = "vd-datepicker-weekdays";
7714
- DAYS.forEach((d) => {
8335
+ weekdays.setAttribute("role", "row");
8336
+ DAYS.forEach(function(d) {
7715
8337
  const span = document.createElement("span");
8338
+ span.setAttribute("role", "columnheader");
8339
+ span.setAttribute("aria-label", d);
7716
8340
  span.textContent = d;
7717
8341
  weekdays.appendChild(span);
7718
8342
  });
7719
- popup.appendChild(weekdays);
7720
- const grid = document.createElement("div");
7721
- grid.className = "vd-datepicker-days";
8343
+ gridWrap.appendChild(weekdays);
7722
8344
  const firstDay = new Date(viewYear, viewMonth, 1).getDay();
7723
8345
  const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
7724
8346
  const daysInPrev = new Date(viewYear, viewMonth, 0).getDate();
8347
+ const cells = [];
7725
8348
  for (let i = firstDay - 1; i >= 0; i--) {
7726
- const btn = createDayBtn(daysInPrev - i, true);
7727
- grid.appendChild(btn);
8349
+ const dayNum = daysInPrev - i;
8350
+ const prevMonth = viewMonth === 0 ? 11 : viewMonth - 1;
8351
+ const prevYear = viewMonth === 0 ? viewYear - 1 : viewYear;
8352
+ const date = new Date(prevYear, prevMonth, dayNum);
8353
+ cells.push({ day: dayNum, outside: true, date });
7728
8354
  }
7729
8355
  for (let d = 1; d <= daysInMonth; d++) {
7730
8356
  const date = new Date(viewYear, viewMonth, d);
7731
- const btn = createDayBtn(d, false, date);
7732
- grid.appendChild(btn);
8357
+ cells.push({ day: d, outside: false, date });
7733
8358
  }
7734
8359
  const totalCells = firstDay + daysInMonth;
7735
8360
  const remaining = totalCells % 7 === 0 ? 0 : 7 - totalCells % 7;
7736
8361
  for (let i = 1; i <= remaining; i++) {
7737
- const btn = createDayBtn(i, true);
7738
- grid.appendChild(btn);
8362
+ const date = new Date(viewYear, viewMonth + 1, i);
8363
+ cells.push({ day: i, outside: true, date });
7739
8364
  }
7740
- popup.appendChild(grid);
8365
+ for (let r = 0; r < cells.length; r += 7) {
8366
+ const row = document.createElement("div");
8367
+ row.className = "vd-datepicker-row";
8368
+ row.setAttribute("role", "row");
8369
+ for (let c = 0; c < 7; c++) {
8370
+ const cell = cells[r + c];
8371
+ row.appendChild(createDayBtn(cell.day, cell.outside, cell.date));
8372
+ }
8373
+ gridWrap.appendChild(row);
8374
+ }
8375
+ popup.appendChild(gridWrap);
7741
8376
  } else if (viewMode === "months") {
7742
8377
  const grid = document.createElement("div");
7743
8378
  grid.className = "vd-datepicker-months";
@@ -7778,65 +8413,125 @@
7778
8413
  popup.appendChild(grid);
7779
8414
  }
7780
8415
  };
7781
- const createDayBtn = (day, outside, date) => {
7782
- const btn = document.createElement("button");
7783
- btn.type = "button";
7784
- btn.className = "vd-datepicker-day";
7785
- btn.textContent = day;
7786
- if (outside) {
7787
- btn.classList.add("is-outside");
7788
- btn.tabIndex = -1;
7789
- return btn;
8416
+ const handleGridKeydown = (e) => {
8417
+ if (!popup.classList.contains("is-open") || viewMode !== "days") return;
8418
+ const grid = popup.querySelector(".vd-datepicker-grid");
8419
+ if (!grid || !grid.contains(e.target)) return;
8420
+ const key = e.key;
8421
+ if (key !== "ArrowLeft" && key !== "ArrowRight" && key !== "ArrowUp" && key !== "ArrowDown" && key !== "Home" && key !== "End" && key !== "PageUp" && key !== "PageDown" && key !== "Enter" && key !== " " && key !== "Escape") {
8422
+ return;
7790
8423
  }
7791
- if (date && isSameDay(date, today)) btn.classList.add("is-today");
7792
- if (date && isSameDay(date, selectedDate)) btn.classList.add("is-selected");
7793
- if (date && isDisabled(date)) {
7794
- btn.classList.add("is-disabled");
7795
- return btn;
8424
+ if (key === "Escape") {
8425
+ e.preventDefault();
8426
+ e.stopPropagation();
8427
+ skipNextFocusOpen = true;
8428
+ close();
8429
+ input.focus();
8430
+ return;
7796
8431
  }
7797
- if (date) {
7798
- btn.addEventListener("click", () => {
7799
- selectedDate = date;
7800
- viewYear = date.getFullYear();
7801
- viewMonth = date.getMonth();
7802
- input.value = formatDate(date);
7803
- close();
7804
- input.dispatchEvent(new CustomEvent("datepicker:select", {
7805
- detail: { date, formatted: input.value },
7806
- bubbles: true
7807
- }));
7808
- input.dispatchEvent(new Event("change", { bubbles: true }));
7809
- });
8432
+ if (!focusedDate) {
8433
+ focusedDate = firstSelectableInMonth(viewYear, viewMonth);
7810
8434
  }
7811
- return btn;
8435
+ if (key === "Enter" || key === " ") {
8436
+ e.preventDefault();
8437
+ if (focusedDate && !isDisabled(focusedDate)) {
8438
+ selectDate(new Date(focusedDate.getFullYear(), focusedDate.getMonth(), focusedDate.getDate()));
8439
+ }
8440
+ return;
8441
+ }
8442
+ e.preventDefault();
8443
+ let next = new Date(focusedDate.getFullYear(), focusedDate.getMonth(), focusedDate.getDate());
8444
+ let skipDir = 1;
8445
+ if (key === "ArrowLeft") {
8446
+ next = addDays(next, -1);
8447
+ skipDir = -1;
8448
+ } else if (key === "ArrowRight") {
8449
+ next = addDays(next, 1);
8450
+ skipDir = 1;
8451
+ } else if (key === "ArrowUp") {
8452
+ next = addDays(next, -7);
8453
+ skipDir = -1;
8454
+ } else if (key === "ArrowDown") {
8455
+ next = addDays(next, 7);
8456
+ skipDir = 1;
8457
+ } else if (key === "Home") {
8458
+ next = startOfWeekSunday(next);
8459
+ skipDir = 1;
8460
+ } else if (key === "End") {
8461
+ next = endOfWeekSunday(next);
8462
+ skipDir = -1;
8463
+ } else if (key === "PageUp") {
8464
+ next = addMonthsClamped(next, -1);
8465
+ skipDir = -1;
8466
+ } else if (key === "PageDown") {
8467
+ next = addMonthsClamped(next, 1);
8468
+ skipDir = 1;
8469
+ }
8470
+ next = skipDisabled(next, skipDir, 400);
8471
+ if (next.getMonth() !== viewMonth || next.getFullYear() !== viewYear) {
8472
+ viewYear = next.getFullYear();
8473
+ viewMonth = next.getMonth();
8474
+ const cl = ensureMonthInRange(viewYear, viewMonth);
8475
+ viewYear = cl.y;
8476
+ viewMonth = cl.m;
8477
+ }
8478
+ focusedDate = next;
8479
+ render();
8480
+ requestAnimationFrame(focusFocusedDay);
7812
8481
  };
7813
8482
  const open = () => {
8483
+ viewMode = "days";
8484
+ if (selectedDate) {
8485
+ viewYear = selectedDate.getFullYear();
8486
+ viewMonth = selectedDate.getMonth();
8487
+ }
8488
+ const cl = ensureMonthInRange(viewYear, viewMonth);
8489
+ viewYear = cl.y;
8490
+ viewMonth = cl.m;
8491
+ if (selectedDate) {
8492
+ focusedDate = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate());
8493
+ } else {
8494
+ focusedDate = firstSelectableInMonth(viewYear, viewMonth);
8495
+ }
7814
8496
  render();
7815
8497
  popup.classList.add("is-open");
7816
8498
  input.setAttribute("aria-expanded", "true");
8499
+ requestAnimationFrame(focusFocusedDay);
7817
8500
  };
7818
8501
  const close = () => {
7819
8502
  popup.classList.remove("is-open");
7820
8503
  input.setAttribute("aria-expanded", "false");
7821
8504
  viewMode = "days";
7822
8505
  };
7823
- const focusHandler = () => open();
8506
+ const focusHandler = () => {
8507
+ if (skipNextFocusOpen) {
8508
+ skipNextFocusOpen = false;
8509
+ return;
8510
+ }
8511
+ open();
8512
+ };
7824
8513
  const outsideHandler = (e) => {
7825
8514
  if (!wrapper.contains(e.target)) close();
7826
8515
  };
7827
8516
  const escHandler = (e) => {
7828
- if (e.key === "Escape") close();
8517
+ if (e.key === "Escape" && popup.classList.contains("is-open")) {
8518
+ skipNextFocusOpen = true;
8519
+ close();
8520
+ input.focus();
8521
+ }
7829
8522
  };
7830
8523
  input.addEventListener("focus", focusHandler);
7831
8524
  document.addEventListener("click", outsideHandler, true);
7832
8525
  document.addEventListener("keydown", escHandler);
8526
+ popup.addEventListener("keydown", handleGridKeydown);
7833
8527
  input.setAttribute("aria-haspopup", "dialog");
7834
8528
  input.setAttribute("aria-expanded", "false");
7835
8529
  input.setAttribute("autocomplete", "off");
7836
8530
  cleanup.push(
7837
8531
  () => input.removeEventListener("focus", focusHandler),
7838
8532
  () => document.removeEventListener("click", outsideHandler, true),
7839
- () => document.removeEventListener("keydown", escHandler)
8533
+ () => document.removeEventListener("keydown", escHandler),
8534
+ () => popup.removeEventListener("keydown", handleGridKeydown)
7840
8535
  );
7841
8536
  this.instances.set(input, { cleanup, open, close, popup });
7842
8537
  },