@vanduo-oss/framework 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vanduo.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! Vanduo v1.3.2 | Built: 2026-04-06T19:04:41.601Z | git:8e08b38 | development */
1
+ /*! Vanduo v1.3.4 | Built: 2026-04-14T21:21:55.517Z | git:73e3db5 | 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.2" : "0.0.0-dev";
110
+ const VANDUO_VERSION = true ? "1.3.4" : "0.0.0-dev";
111
111
  const Vanduo2 = {
112
112
  version: VANDUO_VERSION,
113
113
  components: {},
@@ -2157,6 +2157,31 @@
2157
2157
  this.initNavbar(navbar);
2158
2158
  });
2159
2159
  },
2160
+ /**
2161
+ * Initialize scroll-aware glass/transparent behaviour for a navbar.
2162
+ * Adds/removes `.vd-navbar-scrolled` when the page scrolls past a threshold.
2163
+ * Threshold: `data-scroll-threshold` attribute (px) or the navbar's own height.
2164
+ * @param {HTMLElement} navbar - Navbar element
2165
+ * @returns {Function|null} Cleanup function, or null if not applicable
2166
+ */
2167
+ initScrollWatcher: function(navbar) {
2168
+ const isGlass = navbar.classList.contains("vd-navbar-glass");
2169
+ const isTransparent = navbar.classList.contains("vd-navbar-transparent");
2170
+ if (!isGlass && !isTransparent) {
2171
+ return null;
2172
+ }
2173
+ const getThreshold = () => {
2174
+ const attr = parseInt(navbar.dataset.scrollThreshold, 10);
2175
+ return isNaN(attr) ? navbar.offsetHeight || 60 : attr;
2176
+ };
2177
+ const onScroll = () => {
2178
+ const scrolled = window.scrollY > getThreshold();
2179
+ navbar.classList.toggle("vd-navbar-scrolled", scrolled);
2180
+ };
2181
+ onScroll();
2182
+ window.addEventListener("scroll", onScroll, { passive: true });
2183
+ return () => window.removeEventListener("scroll", onScroll);
2184
+ },
2160
2185
  /**
2161
2186
  * Initialize a single navbar
2162
2187
  * @param {HTMLElement} navbar - Navbar element
@@ -2165,10 +2190,17 @@
2165
2190
  const toggle = navbar.querySelector(".vd-navbar-toggle, .vd-navbar-burger");
2166
2191
  const menu = navbar.querySelector(".vd-navbar-menu");
2167
2192
  const overlay = navbar.querySelector(".vd-navbar-overlay") || this.createOverlay(navbar);
2193
+ const cleanupFunctions = [];
2194
+ const scrollWatcherCleanup = this.initScrollWatcher(navbar);
2195
+ if (scrollWatcherCleanup) {
2196
+ cleanupFunctions.push(scrollWatcherCleanup);
2197
+ }
2168
2198
  if (!toggle || !menu) {
2199
+ if (cleanupFunctions.length) {
2200
+ this.instances.set(navbar, { toggle: null, menu: null, overlay: null, cleanup: cleanupFunctions });
2201
+ }
2169
2202
  return;
2170
2203
  }
2171
- const cleanupFunctions = [];
2172
2204
  const toggleClickHandler = (e) => {
2173
2205
  e.preventDefault();
2174
2206
  e.stopPropagation();
@@ -3806,6 +3838,7 @@
3806
3838
  loadPreferences: function() {
3807
3839
  this.state.theme = this.getStorageValue(this.STORAGE_KEYS.THEME, this.DEFAULTS.THEME);
3808
3840
  this.state.primary = this.getStorageValue(this.STORAGE_KEYS.PRIMARY, this.getDefaultPrimary(this.state.theme));
3841
+ this._normalizeDefaultPrimaryIfStaleWithStoredTheme();
3809
3842
  this.state.neutral = this.getStorageValue(this.STORAGE_KEYS.NEUTRAL, this.DEFAULTS.NEUTRAL);
3810
3843
  this.state.radius = this.getStorageValue(this.STORAGE_KEYS.RADIUS, this.DEFAULTS.RADIUS);
3811
3844
  this.state.font = this.getStorageValue(this.STORAGE_KEYS.FONT, this.DEFAULTS.FONT);
@@ -3891,12 +3924,10 @@
3891
3924
  mode = this.DEFAULTS.THEME;
3892
3925
  }
3893
3926
  this._isApplying = true;
3894
- const currentMode = this.state.theme;
3895
- const oldDefault = this.getDefaultPrimary(currentMode);
3896
- if (this.state.primary === oldDefault) {
3897
- const newDefault = this.getDefaultPrimary(mode);
3898
- if (newDefault !== this.state.primary) {
3899
- this.applyPrimary(newDefault);
3927
+ if (this.isUsingDefaultPrimary()) {
3928
+ const expected = this.getDefaultPrimary(mode);
3929
+ if (this.state.primary !== expected) {
3930
+ this.applyPrimary(expected);
3900
3931
  }
3901
3932
  }
3902
3933
  this.state.theme = mode;
@@ -4138,6 +4169,20 @@
4138
4169
  isUsingDefaultPrimary: function() {
4139
4170
  return this.state.primary === this.DEFAULTS.PRIMARY_LIGHT || this.state.primary === this.DEFAULTS.PRIMARY_DARK;
4140
4171
  },
4172
+ /**
4173
+ * When primary is still one of the auto-default palette keys (black/amber) but
4174
+ * localStorage was written under a different theme (or OS changed in system mode),
4175
+ * align in-memory state before applyAllPreferences runs — avoids amber+light / black+dark drift.
4176
+ */
4177
+ _normalizeDefaultPrimaryIfStaleWithStoredTheme: function() {
4178
+ if (!this.isUsingDefaultPrimary()) {
4179
+ return;
4180
+ }
4181
+ const expected = this.getDefaultPrimary(this.state.theme);
4182
+ if (this.state.primary !== expected) {
4183
+ this.state.primary = expected;
4184
+ }
4185
+ },
4141
4186
  bindEvents: function() {
4142
4187
  if (this.elements.trigger) {
4143
4188
  this.addListener(this.elements.trigger, "click", (e) => {
@@ -4376,6 +4421,9 @@
4376
4421
  this._onMediaChange = (_e) => {
4377
4422
  if (this.state.preference === "system") {
4378
4423
  this.applyTheme();
4424
+ if (window.ThemeCustomizer && typeof window.ThemeCustomizer.applyTheme === "function" && !window.ThemeCustomizer._isApplying) {
4425
+ window.ThemeCustomizer.applyTheme("system");
4426
+ }
4379
4427
  }
4380
4428
  };
4381
4429
  this._mediaQuery.addEventListener("change", this._onMediaChange);
@@ -5607,6 +5655,8 @@
5607
5655
  touchState: null,
5608
5656
  // Feedback element
5609
5657
  feedbackElement: null,
5658
+ // Shared selector used by init and touch reorder
5659
+ containerSelector: ".vd-draggable-container, .vd-draggable-container-vertical",
5610
5660
  /**
5611
5661
  * Initialize draggable components
5612
5662
  */
@@ -5618,7 +5668,7 @@
5618
5668
  }
5619
5669
  this.initDraggable(element);
5620
5670
  });
5621
- const containers = document.querySelectorAll(".vd-draggable-container, .vd-draggable-container-vertical");
5671
+ const containers = document.querySelectorAll(this.containerSelector);
5622
5672
  containers.forEach((container) => {
5623
5673
  if (!this.instances.has(container)) {
5624
5674
  this.initContainer(container);
@@ -5862,10 +5912,16 @@
5862
5912
  */
5863
5913
  handleTouchStart: function(e, element) {
5864
5914
  const touch = e.touches[0];
5915
+ const rect = element.getBoundingClientRect();
5865
5916
  this.touchState = {
5866
5917
  element,
5867
5918
  startX: touch.clientX,
5868
5919
  startY: touch.clientY,
5920
+ lastX: touch.clientX,
5921
+ lastY: touch.clientY,
5922
+ // Keep preview anchored to the original grab point.
5923
+ offsetX: touch.clientX - rect.left,
5924
+ offsetY: touch.clientY - rect.top,
5869
5925
  startTime: Date.now(),
5870
5926
  isDragging: false
5871
5927
  };
@@ -5878,6 +5934,8 @@
5878
5934
  handleTouchMove: function(e, element) {
5879
5935
  if (!this.touchState) return;
5880
5936
  const touch = e.touches[0];
5937
+ this.touchState.lastX = touch.clientX;
5938
+ this.touchState.lastY = touch.clientY;
5881
5939
  const deltaX = touch.clientX - this.touchState.startX;
5882
5940
  const deltaY = touch.clientY - this.touchState.startY;
5883
5941
  if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
@@ -5890,7 +5948,10 @@
5890
5948
  element,
5891
5949
  initialPosition: { x: this.touchState.startX, y: this.touchState.startY },
5892
5950
  initialBounds: element.getBoundingClientRect(),
5893
- data: this.getData(element)
5951
+ data: this.getData(element),
5952
+ // Preserve where inside the element the drag started for accurate ghost positioning.
5953
+ offsetX: this.touchState.offsetX,
5954
+ offsetY: this.touchState.offsetY
5894
5955
  };
5895
5956
  element.dispatchEvent(new CustomEvent("draggable:start", {
5896
5957
  bubbles: true,
@@ -5912,7 +5973,8 @@
5912
5973
  delta: { x: deltaX, y: deltaY }
5913
5974
  }
5914
5975
  }));
5915
- const container = element.closest(".vd-draggable-container");
5976
+ this.updateTouchDropZone(touch.clientX, touch.clientY);
5977
+ const container = element.closest(this.containerSelector);
5916
5978
  if (container && container.contains(element)) {
5917
5979
  this.handleReorder(container, element, touch.clientX, touch.clientY);
5918
5980
  }
@@ -5927,6 +5989,17 @@
5927
5989
  handleTouchEnd: function(e, element) {
5928
5990
  if (this.touchState && this.touchState.isDragging) {
5929
5991
  if (e.cancelable) e.preventDefault();
5992
+ const endTouch = e.changedTouches?.[0];
5993
+ const endPosition = {
5994
+ x: endTouch?.clientX ?? this.touchState.lastX ?? this.touchState.startX,
5995
+ y: endTouch?.clientY ?? this.touchState.lastY ?? this.touchState.startY
5996
+ };
5997
+ const dropZone = this.resolveDropZoneAtPoint(endPosition.x, endPosition.y) || this.touchState.overZone;
5998
+ if (dropZone) {
5999
+ this.dispatchDrop(dropZone, endPosition);
6000
+ } else if (this.touchState.overZone) {
6001
+ this.touchState.overZone.classList.remove("is-drag-over");
6002
+ }
5930
6003
  element.classList.remove("is-dragging");
5931
6004
  element.classList.add("is-dropped");
5932
6005
  element.setAttribute("aria-grabbed", "false");
@@ -5934,7 +6007,6 @@
5934
6007
  if (this.feedbackElement) {
5935
6008
  this.feedbackElement.classList.add("hidden");
5936
6009
  }
5937
- const endTouch = e.changedTouches[0];
5938
6010
  const data = this.currentDrag?.data || this.getData(element);
5939
6011
  const startX = this.touchState?.startX || 0;
5940
6012
  const startY = this.touchState?.startY || 0;
@@ -5943,10 +6015,10 @@
5943
6015
  detail: {
5944
6016
  element,
5945
6017
  data,
5946
- position: { x: endTouch.clientX, y: endTouch.clientY },
6018
+ position: endPosition,
5947
6019
  delta: {
5948
- x: endTouch.clientX - startX,
5949
- y: endTouch.clientY - startY
6020
+ x: endPosition.x - startX,
6021
+ y: endPosition.y - startY
5950
6022
  }
5951
6023
  }
5952
6024
  }));
@@ -5987,6 +6059,58 @@
5987
6059
  */
5988
6060
  handleDrop: function(e, zone) {
5989
6061
  e.preventDefault();
6062
+ this.dispatchDrop(zone, { x: e.clientX, y: e.clientY });
6063
+ },
6064
+ /**
6065
+ * Resolve a drop zone from viewport coordinates
6066
+ * @param {number} x
6067
+ * @param {number} y
6068
+ * @returns {HTMLElement|null}
6069
+ */
6070
+ resolveDropZoneAtPoint: function(x, y) {
6071
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
6072
+ if (typeof document.elementsFromPoint === "function") {
6073
+ const stacked = document.elementsFromPoint(x, y);
6074
+ for (const element of stacked) {
6075
+ const zone = element.closest(".vd-drop-zone");
6076
+ if (zone) return zone;
6077
+ }
6078
+ }
6079
+ const target = document.elementFromPoint(x, y);
6080
+ const targetZone = target ? target.closest(".vd-drop-zone") : null;
6081
+ if (targetZone) return targetZone;
6082
+ const zones = document.querySelectorAll(".vd-drop-zone");
6083
+ for (const zone of zones) {
6084
+ const rect = zone.getBoundingClientRect();
6085
+ if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
6086
+ return zone;
6087
+ }
6088
+ }
6089
+ return null;
6090
+ },
6091
+ /**
6092
+ * Track and update active drop-zone hover state on touch devices
6093
+ * @param {number} x
6094
+ * @param {number} y
6095
+ */
6096
+ updateTouchDropZone: function(x, y) {
6097
+ if (!this.touchState) return;
6098
+ const nextZone = this.resolveDropZoneAtPoint(x, y);
6099
+ const prevZone = this.touchState.overZone || null;
6100
+ if (prevZone && prevZone !== nextZone) {
6101
+ prevZone.classList.remove("is-drag-over");
6102
+ }
6103
+ if (nextZone && nextZone !== prevZone) {
6104
+ nextZone.classList.add("is-drag-over");
6105
+ }
6106
+ this.touchState.overZone = nextZone || null;
6107
+ },
6108
+ /**
6109
+ * Dispatch a normalized drop event for mouse and touch flows
6110
+ * @param {HTMLElement} zone
6111
+ * @param {{x:number, y:number}} position
6112
+ */
6113
+ dispatchDrop: function(zone, position) {
5990
6114
  zone.classList.remove("is-drag-over");
5991
6115
  zone.dispatchEvent(new CustomEvent("draggable:drop", {
5992
6116
  bubbles: true,
@@ -5994,7 +6118,7 @@
5994
6118
  zone,
5995
6119
  element: this.currentDrag?.element,
5996
6120
  data: this.currentDrag?.data,
5997
- position: { x: e.clientX, y: e.clientY }
6121
+ position
5998
6122
  }
5999
6123
  }));
6000
6124
  },
@@ -6095,9 +6219,11 @@
6095
6219
  this.feedbackElement.innerHTML = "";
6096
6220
  const clone = this.currentDrag.element.cloneNode(true);
6097
6221
  this.feedbackElement.appendChild(clone);
6222
+ const offsetX = this.currentDrag.offsetX ?? 20;
6223
+ const offsetY = this.currentDrag.offsetY ?? 20;
6098
6224
  Object.assign(this.feedbackElement.style, {
6099
- left: x - 20 + "px",
6100
- top: y - 20 + "px",
6225
+ left: x - offsetX + "px",
6226
+ top: y - offsetY + "px",
6101
6227
  width: rect.width + "px",
6102
6228
  height: rect.height + "px"
6103
6229
  });
@@ -6385,6 +6511,67 @@
6385
6511
  window.VanduoLazyLoad = VanduoLazyLoad;
6386
6512
  })();
6387
6513
 
6514
+ // js/components/glass.js
6515
+ (function() {
6516
+ "use strict";
6517
+ const GlassScroll = {
6518
+ /** @type {Map<Element, IntersectionObserver>} */
6519
+ observers: /* @__PURE__ */ new Map(),
6520
+ init: function() {
6521
+ document.querySelectorAll("[data-glass-scroll]").forEach((el) => {
6522
+ if (this.observers.has(el)) return;
6523
+ this.initElement(el);
6524
+ });
6525
+ },
6526
+ /**
6527
+ * Wire up a single scroll-activated glass element.
6528
+ * @param {HTMLElement} el
6529
+ */
6530
+ initElement: function(el) {
6531
+ const sentinelSelector = el.dataset.glassSentinel;
6532
+ let sentinel;
6533
+ if (sentinelSelector) {
6534
+ sentinel = document.querySelector(sentinelSelector);
6535
+ }
6536
+ if (!sentinel) {
6537
+ sentinel = el.previousElementSibling;
6538
+ }
6539
+ if (!sentinel) {
6540
+ el.classList.add("is-glass-active");
6541
+ return;
6542
+ }
6543
+ const observer = new IntersectionObserver(
6544
+ (entries) => {
6545
+ entries.forEach((entry) => {
6546
+ el.classList.toggle("is-glass-active", !entry.isIntersecting);
6547
+ });
6548
+ },
6549
+ { threshold: 0, rootMargin: "0px" }
6550
+ );
6551
+ observer.observe(sentinel);
6552
+ this.observers.set(el, observer);
6553
+ },
6554
+ /**
6555
+ * Disconnect and remove a single element's observer.
6556
+ * @param {HTMLElement} el
6557
+ */
6558
+ destroy: function(el) {
6559
+ const observer = this.observers.get(el);
6560
+ if (observer) {
6561
+ observer.disconnect();
6562
+ this.observers.delete(el);
6563
+ }
6564
+ },
6565
+ destroyAll: function() {
6566
+ this.observers.forEach((observer, el) => this.destroy(el));
6567
+ }
6568
+ };
6569
+ if (typeof window.Vanduo !== "undefined") {
6570
+ window.Vanduo.register("glassScroll", GlassScroll);
6571
+ }
6572
+ window.VanduoGlassScroll = GlassScroll;
6573
+ })();
6574
+
6388
6575
  // js/components/flow.js
6389
6576
  (function() {
6390
6577
  "use strict";