@vanduo-oss/framework 1.3.3 → 1.3.5

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.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Vanduo Framework - Glass Scroll Activation
3
+ * Generic scroll-aware glass activation via IntersectionObserver.
4
+ *
5
+ * Usage:
6
+ * Add `data-glass-scroll` to any element carrying a `.vd-glass*` class.
7
+ * By default the previous sibling is used as the sentinel.
8
+ * Point to a custom sentinel via `data-glass-sentinel="<CSS selector>"`.
9
+ *
10
+ * Behaviour:
11
+ * - Adds `.is-glass-active` when the sentinel leaves the viewport (scrolled past).
12
+ * - Removes `.is-glass-active` when the sentinel re-enters the viewport.
13
+ */
14
+ (function () {
15
+ 'use strict';
16
+
17
+ const GlassScroll = {
18
+ /** @type {Map<Element, IntersectionObserver>} */
19
+ observers: new Map(),
20
+
21
+ init: function () {
22
+ document.querySelectorAll('[data-glass-scroll]').forEach(el => {
23
+ if (this.observers.has(el)) return;
24
+ this.initElement(el);
25
+ });
26
+ },
27
+
28
+ /**
29
+ * Wire up a single scroll-activated glass element.
30
+ * @param {HTMLElement} el
31
+ */
32
+ initElement: function (el) {
33
+ const sentinelSelector = el.dataset.glassSentinel;
34
+ let sentinel;
35
+
36
+ if (sentinelSelector) {
37
+ sentinel = document.querySelector(sentinelSelector);
38
+ }
39
+
40
+ if (!sentinel) {
41
+ // Fall back to the previous sibling element
42
+ sentinel = el.previousElementSibling;
43
+ }
44
+
45
+ if (!sentinel) {
46
+ // No sentinel available — activate immediately so glass is always shown
47
+ el.classList.add('is-glass-active');
48
+ return;
49
+ }
50
+
51
+ const observer = new IntersectionObserver(
52
+ (entries) => {
53
+ entries.forEach(entry => {
54
+ // Active when sentinel is NOT intersecting (scrolled past it)
55
+ el.classList.toggle('is-glass-active', !entry.isIntersecting);
56
+ });
57
+ },
58
+ { threshold: 0, rootMargin: '0px' }
59
+ );
60
+
61
+ observer.observe(sentinel);
62
+ this.observers.set(el, observer);
63
+ },
64
+
65
+ /**
66
+ * Disconnect and remove a single element's observer.
67
+ * @param {HTMLElement} el
68
+ */
69
+ destroy: function (el) {
70
+ const observer = this.observers.get(el);
71
+ if (observer) {
72
+ observer.disconnect();
73
+ this.observers.delete(el);
74
+ }
75
+ },
76
+
77
+ destroyAll: function () {
78
+ this.observers.forEach((observer, el) => this.destroy(el));
79
+ }
80
+ };
81
+
82
+ if (typeof window.Vanduo !== 'undefined') {
83
+ window.Vanduo.register('glassScroll', GlassScroll);
84
+ }
85
+
86
+ window.VanduoGlassScroll = GlassScroll;
87
+ })();
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Vanduo Framework - Water Morph Effect Component
3
+ * Liquid wave content-swap animation on click
4
+ *
5
+ * Usage:
6
+ * Add .vd-morph or [data-vd-morph] to any element.
7
+ * Provide .vd-morph-content.vd-morph-current and .vd-morph-content.vd-morph-next children.
8
+ * Wave and shine layers are auto-created if absent.
9
+ *
10
+ * JS API:
11
+ * VanduoMorph.morph(el) — trigger morph programmatically (from center)
12
+ * VanduoMorph.destroy(el) — tear down listeners for one element
13
+ * VanduoMorph.destroyAll() — tear down all instances
14
+ */
15
+
16
+ (function () {
17
+ 'use strict';
18
+
19
+ const MORPH_DURATION_MS = 750;
20
+
21
+ const Morph = {
22
+ instances: new Map(),
23
+
24
+ init: function () {
25
+ const elements = document.querySelectorAll('.vd-morph, [data-vd-morph]');
26
+ elements.forEach(function (el) {
27
+ if (Morph.instances.has(el)) return;
28
+ if (el.getAttribute('data-vd-morph') === 'manual') return;
29
+ Morph.initInstance(el);
30
+ });
31
+ },
32
+
33
+ initInstance: function (el) {
34
+ Morph._ensureLayers(el);
35
+
36
+ const cleanup = [];
37
+ let morphing = false;
38
+
39
+ const handleClick = function (e) {
40
+ if (morphing) return;
41
+ Morph._runMorph(el, e, function () { morphing = false; });
42
+ morphing = true;
43
+ };
44
+
45
+ el.addEventListener('click', handleClick);
46
+ cleanup.push(function () { el.removeEventListener('click', handleClick); });
47
+
48
+ this.instances.set(el, { cleanup: cleanup });
49
+ },
50
+
51
+ morph: function (el) {
52
+ if (!el) return;
53
+ if (!this.instances.has(el)) this.initInstance(el);
54
+ this._runMorph(el, null, null);
55
+ },
56
+
57
+ destroy: function (el) {
58
+ const instance = this.instances.get(el);
59
+ if (!instance) return;
60
+ instance.cleanup.forEach(function (fn) { fn(); });
61
+ this.instances.delete(el);
62
+ },
63
+
64
+ destroyAll: function () {
65
+ this.instances.forEach(function (_, el) { Morph.destroy(el); });
66
+ },
67
+
68
+ /* ── Internal helpers ── */
69
+
70
+ _ensureLayers: function (el) {
71
+ if (!el.querySelector('.vd-morph-wave')) {
72
+ const wave = document.createElement('span');
73
+ wave.className = 'vd-morph-wave';
74
+ wave.setAttribute('aria-hidden', 'true');
75
+ el.insertBefore(wave, el.firstChild);
76
+ }
77
+ if (!el.querySelector('.vd-morph-shine')) {
78
+ const shine = document.createElement('span');
79
+ shine.className = 'vd-morph-shine';
80
+ shine.setAttribute('aria-hidden', 'true');
81
+ const waveEl = el.querySelector('.vd-morph-wave');
82
+ if (waveEl && waveEl.nextSibling) {
83
+ el.insertBefore(shine, waveEl.nextSibling);
84
+ } else {
85
+ el.insertBefore(shine, el.firstChild);
86
+ }
87
+ }
88
+ },
89
+
90
+ _runMorph: function (el, pointerEvent, onComplete) {
91
+ const wave = el.querySelector('.vd-morph-wave');
92
+ if (wave) {
93
+ const rect = el.getBoundingClientRect();
94
+ const cx = rect.left + rect.width / 2;
95
+ const cy = rect.top + rect.height / 2;
96
+ const px = pointerEvent ? (pointerEvent.clientX || cx) : cx;
97
+ const py = pointerEvent ? (pointerEvent.clientY || cy) : cy;
98
+ wave.style.left = (px - rect.left) + 'px';
99
+ wave.style.top = (py - rect.top) + 'px';
100
+ }
101
+
102
+ el.classList.add('is-morphing');
103
+
104
+ let duration = MORPH_DURATION_MS;
105
+ const custom = getComputedStyle(el).getPropertyValue('--morph-duration');
106
+ if (custom) {
107
+ const parsed = parseFloat(custom);
108
+ if (!isNaN(parsed)) duration = parsed * (custom.indexOf('ms') !== -1 ? 1 : 1000);
109
+ }
110
+
111
+ setTimeout(function () {
112
+ el.classList.remove('is-morphing');
113
+
114
+ const current = el.querySelector('.vd-morph-current');
115
+ const next = el.querySelector('.vd-morph-next');
116
+ if (current && next) {
117
+ current.classList.remove('vd-morph-current');
118
+ current.classList.add('vd-morph-next');
119
+ next.classList.remove('vd-morph-next');
120
+ next.classList.add('vd-morph-current');
121
+ }
122
+
123
+ el.classList.add('morph-done');
124
+ setTimeout(function () { el.classList.remove('morph-done'); }, 350);
125
+
126
+ if (typeof onComplete === 'function') onComplete();
127
+ }, duration);
128
+ }
129
+ };
130
+
131
+ if (typeof window.Vanduo !== 'undefined') {
132
+ window.Vanduo.register('morph', Morph);
133
+ }
134
+
135
+ window.VanduoMorph = Morph;
136
+
137
+ })();
@@ -41,6 +41,37 @@
41
41
  });
42
42
  },
43
43
 
44
+ /**
45
+ * Initialize scroll-aware glass/transparent behaviour for a navbar.
46
+ * Adds/removes `.vd-navbar-scrolled` when the page scrolls past a threshold.
47
+ * Threshold: `data-scroll-threshold` attribute (px) or the navbar's own height.
48
+ * @param {HTMLElement} navbar - Navbar element
49
+ * @returns {Function|null} Cleanup function, or null if not applicable
50
+ */
51
+ initScrollWatcher: function (navbar) {
52
+ const isGlass = navbar.classList.contains('vd-navbar-glass');
53
+ const isTransparent = navbar.classList.contains('vd-navbar-transparent');
54
+
55
+ if (!isGlass && !isTransparent) {
56
+ return null;
57
+ }
58
+
59
+ const getThreshold = () => {
60
+ const attr = parseInt(navbar.dataset.scrollThreshold, 10);
61
+ return isNaN(attr) ? (navbar.offsetHeight || 60) : attr;
62
+ };
63
+
64
+ const onScroll = () => {
65
+ const scrolled = window.scrollY > getThreshold();
66
+ navbar.classList.toggle('vd-navbar-scrolled', scrolled);
67
+ };
68
+
69
+ onScroll(); // set initial state without waiting for first scroll
70
+ window.addEventListener('scroll', onScroll, { passive: true });
71
+
72
+ return () => window.removeEventListener('scroll', onScroll);
73
+ },
74
+
44
75
  /**
45
76
  * Initialize a single navbar
46
77
  * @param {HTMLElement} navbar - Navbar element
@@ -50,13 +81,23 @@
50
81
  const menu = navbar.querySelector('.vd-navbar-menu');
51
82
  const overlay = navbar.querySelector('.vd-navbar-overlay') || this.createOverlay(navbar);
52
83
 
84
+ // Store cleanup functions for this navbar instance
85
+ const cleanupFunctions = [];
86
+
87
+ // Wire up scroll-aware glass/transparent behaviour regardless of mobile menu
88
+ const scrollWatcherCleanup = this.initScrollWatcher(navbar);
89
+ if (scrollWatcherCleanup) {
90
+ cleanupFunctions.push(scrollWatcherCleanup);
91
+ }
92
+
53
93
  if (!toggle || !menu) {
94
+ // Still store the instance so scroll-watcher cleanup is tracked
95
+ if (cleanupFunctions.length) {
96
+ this.instances.set(navbar, { toggle: null, menu: null, overlay: null, cleanup: cleanupFunctions });
97
+ }
54
98
  return;
55
99
  }
56
100
 
57
- // Store cleanup functions for this navbar instance
58
- const cleanupFunctions = [];
59
-
60
101
  // Toggle menu on button click
61
102
  const toggleClickHandler = (e) => {
62
103
  e.preventDefault();
package/js/index.js CHANGED
@@ -47,6 +47,10 @@ import './components/doc-search.js';
47
47
  import './components/draggable.js';
48
48
  import './components/lazy-load.js';
49
49
 
50
+ // Effects (glass scroll activation, water morph)
51
+ import './components/glass.js';
52
+ import './components/morph.js';
53
+
50
54
  // Phase 10 (v1.2.7) components
51
55
  import './components/flow.js';
52
56
  import './components/bubble.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanduo-oss/framework",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "Zero-dependency CSS/JS framework built on Fibonacci/Golden Ratio design system with Open Color integration",
5
5
  "keywords": [
6
6
  "css",