@vanduo-oss/framework 1.3.5 → 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.
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Vanduo Framework — Expanding flex cards
3
+ * Click / Enter / Space / Arrow keys to change the active card.
4
+ *
5
+ * Usage:
6
+ * <div class="vd-expanding-cards" data-vd-expanding-cards>
7
+ * Use data-vd-expanding-cards="manual" to skip auto-init.
8
+ */
9
+
10
+ (function () {
11
+ 'use strict';
12
+
13
+ const ExpandingCards = {
14
+ instances: new Map(),
15
+
16
+ init: function () {
17
+ document.querySelectorAll('.vd-expanding-cards').forEach(function (el) {
18
+ if (el.getAttribute('data-vd-expanding-cards') === 'manual') return;
19
+ if (ExpandingCards.instances.has(el)) return;
20
+ ExpandingCards.initContainer(el);
21
+ });
22
+ },
23
+
24
+ initContainer: function (container) {
25
+ const cleanup = [];
26
+
27
+ const getCards = function () {
28
+ return Array.prototype.slice.call(container.querySelectorAll('.vd-expanding-card'));
29
+ };
30
+
31
+ const setActive = function (card) {
32
+ const cards = getCards();
33
+ if (!card || cards.indexOf(card) === -1) return;
34
+ cards.forEach(function (c) {
35
+ c.classList.toggle('is-active', c === card);
36
+ });
37
+ card.focus({ preventScroll: true });
38
+ };
39
+
40
+ const onClick = function (e) {
41
+ const t = e.target;
42
+ const card = t.closest ? t.closest('.vd-expanding-card') : null;
43
+ if (!card || !container.contains(card)) return;
44
+ setActive(card);
45
+ };
46
+
47
+ const onKeydown = function (e) {
48
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight' && e.key !== 'Home' && e.key !== 'End') {
49
+ return;
50
+ }
51
+ const cards = getCards().filter(function (c) {
52
+ return c.offsetParent !== null || c.getClientRects().length > 0;
53
+ });
54
+ if (!cards.length) return;
55
+ const activeEl = document.activeElement;
56
+ let idx = cards.indexOf(activeEl);
57
+ if (idx < 0) {
58
+ idx = cards.findIndex(function (c) {
59
+ return c.classList.contains('is-active');
60
+ });
61
+ }
62
+ if (idx < 0) idx = 0;
63
+
64
+ if (e.key === 'ArrowLeft') {
65
+ e.preventDefault();
66
+ setActive(cards[Math.max(0, idx - 1)]);
67
+ } else if (e.key === 'ArrowRight') {
68
+ e.preventDefault();
69
+ setActive(cards[Math.min(cards.length - 1, idx + 1)]);
70
+ } else if (e.key === 'Home') {
71
+ e.preventDefault();
72
+ setActive(cards[0]);
73
+ } else if (e.key === 'End') {
74
+ e.preventDefault();
75
+ setActive(cards[cards.length - 1]);
76
+ }
77
+ };
78
+
79
+ container.addEventListener('click', onClick);
80
+ cleanup.push(function () {
81
+ container.removeEventListener('click', onClick);
82
+ });
83
+
84
+ container.addEventListener('keydown', onKeydown);
85
+ cleanup.push(function () {
86
+ container.removeEventListener('keydown', onKeydown);
87
+ });
88
+
89
+ getCards().forEach(function (card) {
90
+ if (!card.hasAttribute('tabindex')) {
91
+ card.setAttribute('tabindex', '0');
92
+ }
93
+ card.setAttribute('role', 'button');
94
+ if (!card.hasAttribute('aria-pressed')) {
95
+ card.setAttribute('aria-pressed', card.classList.contains('is-active') ? 'true' : 'false');
96
+ }
97
+ });
98
+
99
+ const syncAria = function () {
100
+ getCards().forEach(function (card) {
101
+ card.setAttribute('aria-pressed', card.classList.contains('is-active') ? 'true' : 'false');
102
+ });
103
+ };
104
+
105
+ const mo = new MutationObserver(syncAria);
106
+ mo.observe(container, { attributes: true, subtree: true, attributeFilter: ['class'] });
107
+ cleanup.push(function () {
108
+ mo.disconnect();
109
+ });
110
+ syncAria();
111
+
112
+ ExpandingCards.instances.set(container, { cleanup: cleanup });
113
+ },
114
+
115
+ destroy: function (container) {
116
+ const inst = this.instances.get(container);
117
+ if (!inst) return;
118
+ inst.cleanup.forEach(function (fn) {
119
+ fn();
120
+ });
121
+ this.instances.delete(container);
122
+ },
123
+
124
+ destroyAll: function () {
125
+ this.instances.forEach(function (_, el) {
126
+ ExpandingCards.destroy(el);
127
+ });
128
+ }
129
+ };
130
+
131
+ if (typeof window.Vanduo !== 'undefined') {
132
+ window.Vanduo.register('expandingCards', ExpandingCards);
133
+ }
134
+
135
+ window.VanduoExpandingCards = ExpandingCards;
136
+ })();
@@ -120,9 +120,6 @@
120
120
  next.classList.add('vd-morph-current');
121
121
  }
122
122
 
123
- el.classList.add('morph-done');
124
- setTimeout(function () { el.classList.remove('morph-done'); }, 350);
125
-
126
123
  if (typeof onComplete === 'function') onComplete();
127
124
  }, duration);
128
125
  }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Vanduo Framework — Timeline animated reveal
3
+ * Opt-in: add `.vd-timeline-animated` to a `.vd-timeline` container.
4
+ * Uses IntersectionObserver to add `.is-revealed` per item with staggered delays.
5
+ * Optional: `.vd-timeline-playback` with `[data-vd-timeline-prev|next|play|pause]` for stepped control.
6
+ */
7
+
8
+ (function () {
9
+ 'use strict';
10
+
11
+ const STAGGER_MS = 140;
12
+ const MAX_STAGGER_INDEX = 7;
13
+ const PLAY_INTERVAL_MS = 800;
14
+
15
+ function countRevealedPrefix(items) {
16
+ let count = 0;
17
+ for (let i = 0; i < items.length; i++) {
18
+ if (!items[i].classList.contains('is-revealed')) break;
19
+ count++;
20
+ }
21
+ return count;
22
+ }
23
+
24
+ function findPlaybackControls(container) {
25
+ return container.parentElement || document.body;
26
+ }
27
+
28
+ function initPlayback(container, items, cleanup) {
29
+ items.forEach(function (item) {
30
+ item.classList.remove('is-revealed');
31
+ });
32
+
33
+ const scope = findPlaybackControls(container);
34
+ const prevBtn = scope.querySelector('[data-vd-timeline-prev]');
35
+ const nextBtn = scope.querySelector('[data-vd-timeline-next]');
36
+ const playBtn = scope.querySelector('[data-vd-timeline-play]');
37
+ const pauseBtn = scope.querySelector('[data-vd-timeline-pause]');
38
+
39
+ let playTimer = null;
40
+
41
+ function updateNavButtons() {
42
+ const k = countRevealedPrefix(items);
43
+ const n = items.length;
44
+ if (prevBtn) {
45
+ const atStart = k === 0;
46
+ prevBtn.disabled = atStart;
47
+ prevBtn.setAttribute('aria-disabled', atStart ? 'true' : 'false');
48
+ }
49
+ if (nextBtn) {
50
+ const atEnd = k >= n;
51
+ nextBtn.disabled = atEnd;
52
+ nextBtn.setAttribute('aria-disabled', atEnd ? 'true' : 'false');
53
+ }
54
+ if (playBtn) {
55
+ playBtn.setAttribute('aria-pressed', playTimer ? 'true' : 'false');
56
+ }
57
+ if (pauseBtn) {
58
+ pauseBtn.disabled = !playTimer;
59
+ }
60
+ }
61
+
62
+ function stepNext() {
63
+ const k = countRevealedPrefix(items);
64
+ if (k < items.length) {
65
+ items[k].classList.add('is-revealed');
66
+ }
67
+ updateNavButtons();
68
+ }
69
+
70
+ function stepPrev() {
71
+ const k = countRevealedPrefix(items);
72
+ if (k > 0) {
73
+ items[k - 1].classList.remove('is-revealed');
74
+ }
75
+ updateNavButtons();
76
+ }
77
+
78
+ function play() {
79
+ if (playTimer) return;
80
+ playTimer = setInterval(function () {
81
+ if (countRevealedPrefix(items) >= items.length) {
82
+ pause();
83
+ return;
84
+ }
85
+ stepNext();
86
+ }, PLAY_INTERVAL_MS);
87
+ updateNavButtons();
88
+ }
89
+
90
+ function pause() {
91
+ if (playTimer) {
92
+ clearInterval(playTimer);
93
+ playTimer = null;
94
+ }
95
+ updateNavButtons();
96
+ }
97
+
98
+ function addClick(el, fn) {
99
+ if (!el) return;
100
+ const handler = function (e) {
101
+ e.preventDefault();
102
+ fn();
103
+ };
104
+ el.addEventListener('click', handler);
105
+ cleanup.push(function () {
106
+ el.removeEventListener('click', handler);
107
+ });
108
+ }
109
+
110
+ addClick(prevBtn, stepPrev);
111
+ addClick(nextBtn, stepNext);
112
+ addClick(playBtn, play);
113
+ addClick(pauseBtn, pause);
114
+
115
+ cleanup.push(function () {
116
+ pause();
117
+ });
118
+
119
+ updateNavButtons();
120
+
121
+ return {
122
+ stepNext: stepNext,
123
+ stepPrev: stepPrev,
124
+ play: play,
125
+ pause: pause
126
+ };
127
+ }
128
+
129
+ const Timeline = {
130
+ instances: new Map(),
131
+
132
+ init: function () {
133
+ document.querySelectorAll('.vd-timeline.vd-timeline-animated').forEach(function (el) {
134
+ if (Timeline.instances.has(el)) return;
135
+ Timeline.initInstance(el);
136
+ });
137
+ },
138
+
139
+ reinit: function () {
140
+ Timeline.destroyAll();
141
+ Timeline.init();
142
+ },
143
+
144
+ initInstance: function (container) {
145
+ const cleanup = [];
146
+ const items = Array.prototype.filter.call(container.children, function (child) {
147
+ return child.classList && child.classList.contains('vd-timeline-item');
148
+ });
149
+
150
+ items.forEach(function (item, i) {
151
+ const idx = Math.min(i, MAX_STAGGER_INDEX);
152
+ item.style.setProperty('--vd-timeline-reveal-delay', (idx * STAGGER_MS) + 'ms');
153
+ });
154
+
155
+ const reducedMotion = typeof window.matchMedia === 'function'
156
+ && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
157
+
158
+ if (reducedMotion) {
159
+ items.forEach(function (item) {
160
+ item.classList.add('is-revealed');
161
+ });
162
+ Timeline.instances.set(container, { cleanup: cleanup });
163
+ return;
164
+ }
165
+
166
+ const playback = container.classList && container.classList.contains('vd-timeline-playback');
167
+
168
+ if (playback) {
169
+ const playbackApi = initPlayback(container, items, cleanup);
170
+ Timeline.instances.set(container, { cleanup: cleanup, playback: playbackApi });
171
+ return;
172
+ }
173
+
174
+ if (typeof IntersectionObserver === 'undefined') {
175
+ items.forEach(function (item) {
176
+ item.classList.add('is-revealed');
177
+ });
178
+ Timeline.instances.set(container, { cleanup: cleanup });
179
+ return;
180
+ }
181
+
182
+ const observer = new IntersectionObserver(function (entries) {
183
+ entries.forEach(function (entry) {
184
+ if (!entry.isIntersecting) return;
185
+ entry.target.classList.add('is-revealed');
186
+ observer.unobserve(entry.target);
187
+ });
188
+ }, {
189
+ root: null,
190
+ rootMargin: '0px 0px -10% 0px',
191
+ threshold: 0.15
192
+ });
193
+
194
+ items.forEach(function (item) {
195
+ observer.observe(item);
196
+ });
197
+
198
+ cleanup.push(function () {
199
+ observer.disconnect();
200
+ });
201
+
202
+ Timeline.instances.set(container, { cleanup: cleanup });
203
+ },
204
+
205
+ destroy: function (container) {
206
+ const inst = this.instances.get(container);
207
+ if (!inst) return;
208
+ inst.cleanup.forEach(function (fn) {
209
+ fn();
210
+ });
211
+ this.instances.delete(container);
212
+ },
213
+
214
+ destroyAll: function () {
215
+ this.instances.forEach(function (_, el) {
216
+ Timeline.destroy(el);
217
+ });
218
+ }
219
+ };
220
+
221
+ if (typeof window.Vanduo !== 'undefined') {
222
+ window.Vanduo.register('timeline', Timeline);
223
+ }
224
+
225
+ window.VanduoTimeline = Timeline;
226
+ })();
package/js/index.js CHANGED
@@ -50,6 +50,8 @@ import './components/lazy-load.js';
50
50
  // Effects (glass scroll activation, water morph)
51
51
  import './components/glass.js';
52
52
  import './components/morph.js';
53
+ import './components/expanding-cards.js';
54
+ import './components/timeline.js';
53
55
 
54
56
  // Phase 10 (v1.2.7) components
55
57
  import './components/flow.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanduo-oss/framework",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
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",