@vanduo-oss/framework 1.2.6 → 1.2.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.
Files changed (52) hide show
  1. package/README.md +31 -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 +2152 -4
  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 +1943 -1
  25. package/dist/vanduo.css.map +1 -1
  26. package/dist/vanduo.esm.js +2152 -4
  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 +2152 -4
  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/timepicker.js +142 -0
  47. package/js/components/transfer.js +206 -0
  48. package/js/components/tree.js +191 -0
  49. package/js/components/validate.js +185 -0
  50. package/js/components/waypoint.js +120 -0
  51. package/js/index.js +16 -0
  52. package/package.json +1 -1
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Vanduo Framework - Flow (Carousel/Slider) Component
3
+ * Touch-enabled carousel with slide/fade transitions, autoplay, indicators
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ const Flow = {
10
+ instances: new Map(),
11
+
12
+ init: function () {
13
+ const carousels = document.querySelectorAll('.vd-flow, .vd-carousel');
14
+ carousels.forEach(el => {
15
+ if (this.instances.has(el)) return;
16
+ this.initInstance(el);
17
+ });
18
+ },
19
+
20
+ initInstance: function (el) {
21
+ const track = el.querySelector('.vd-flow-track');
22
+ if (!track) return;
23
+
24
+ const slides = Array.from(track.querySelectorAll('.vd-flow-slide'));
25
+ if (slides.length === 0) return;
26
+
27
+ const isFade = el.classList.contains('vd-flow-fade');
28
+ const autoplay = el.hasAttribute('data-vd-autoplay');
29
+ const interval = parseInt(el.getAttribute('data-vd-interval'), 10) || 5000;
30
+ const loop = el.getAttribute('data-vd-loop') !== 'false';
31
+
32
+ const state = {
33
+ current: 0,
34
+ total: slides.length,
35
+ autoplayTimer: null,
36
+ isFade: isFade,
37
+ loop: loop,
38
+ isDragging: false,
39
+ startX: 0,
40
+ currentX: 0,
41
+ threshold: 50
42
+ };
43
+
44
+ const cleanup = [];
45
+
46
+ // Set initial active slide
47
+ slides.forEach((slide, i) => {
48
+ slide.setAttribute('role', 'group');
49
+ slide.setAttribute('aria-roledescription', 'slide');
50
+ slide.setAttribute('aria-label', 'Slide ' + (i + 1) + ' of ' + slides.length);
51
+ if (i === 0) slide.classList.add('is-active');
52
+ });
53
+
54
+ el.setAttribute('role', 'region');
55
+ el.setAttribute('aria-roledescription', 'carousel');
56
+ if (!el.getAttribute('aria-label')) {
57
+ el.setAttribute('aria-label', 'Carousel');
58
+ }
59
+
60
+ // Live region for announcements
61
+ const liveRegion = document.createElement('div');
62
+ liveRegion.setAttribute('aria-live', 'polite');
63
+ liveRegion.setAttribute('aria-atomic', 'true');
64
+ liveRegion.className = 'sr-only';
65
+ liveRegion.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);';
66
+ el.appendChild(liveRegion);
67
+
68
+ const goTo = (index, announce) => {
69
+ if (announce === undefined) announce = true;
70
+ let target = index;
71
+ if (state.loop) {
72
+ target = ((index % state.total) + state.total) % state.total;
73
+ } else {
74
+ target = Math.max(0, Math.min(index, state.total - 1));
75
+ }
76
+
77
+ const prev = state.current;
78
+ state.current = target;
79
+
80
+ if (state.isFade) {
81
+ slides.forEach((s, i) => {
82
+ s.classList.toggle('is-active', i === target);
83
+ });
84
+ } else {
85
+ track.style.transform = 'translateX(-' + (target * 100) + '%)';
86
+ }
87
+
88
+ // Update indicators
89
+ const indicators = el.querySelectorAll('.vd-flow-indicator');
90
+ indicators.forEach((ind, i) => {
91
+ ind.classList.toggle('is-active', i === target);
92
+ ind.setAttribute('aria-selected', i === target ? 'true' : 'false');
93
+ });
94
+
95
+ // Update slide ARIA
96
+ slides.forEach((s, i) => {
97
+ s.setAttribute('aria-hidden', i !== target ? 'true' : 'false');
98
+ });
99
+
100
+ if (announce) {
101
+ liveRegion.textContent = 'Slide ' + (target + 1) + ' of ' + state.total;
102
+ }
103
+
104
+ el.dispatchEvent(new CustomEvent('flow:change', {
105
+ detail: { current: target, previous: prev, total: state.total }
106
+ }));
107
+ };
108
+
109
+ const next = () => goTo(state.current + 1);
110
+ const prev = () => goTo(state.current - 1);
111
+
112
+ // Controls
113
+ const prevBtn = el.querySelector('.vd-flow-prev');
114
+ const nextBtn = el.querySelector('.vd-flow-next');
115
+
116
+ if (prevBtn) {
117
+ const h = () => prev();
118
+ prevBtn.addEventListener('click', h);
119
+ cleanup.push(() => prevBtn.removeEventListener('click', h));
120
+ }
121
+ if (nextBtn) {
122
+ const h = () => next();
123
+ nextBtn.addEventListener('click', h);
124
+ cleanup.push(() => nextBtn.removeEventListener('click', h));
125
+ }
126
+
127
+ // Indicators
128
+ const indicators = el.querySelectorAll('.vd-flow-indicator');
129
+ indicators.forEach((ind, i) => {
130
+ ind.setAttribute('role', 'tab');
131
+ ind.setAttribute('aria-selected', i === 0 ? 'true' : 'false');
132
+ ind.setAttribute('aria-label', 'Go to slide ' + (i + 1));
133
+ const h = () => goTo(i);
134
+ ind.addEventListener('click', h);
135
+ cleanup.push(() => ind.removeEventListener('click', h));
136
+ });
137
+
138
+ // Keyboard navigation
139
+ const keyHandler = (e) => {
140
+ if (e.key === 'ArrowLeft') { prev(); e.preventDefault(); }
141
+ if (e.key === 'ArrowRight') { next(); e.preventDefault(); }
142
+ };
143
+ el.setAttribute('tabindex', '0');
144
+ el.addEventListener('keydown', keyHandler);
145
+ cleanup.push(() => el.removeEventListener('keydown', keyHandler));
146
+
147
+ // Touch / pointer support
148
+ const pointerDown = (e) => {
149
+ state.isDragging = true;
150
+ state.startX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
151
+ state.currentX = state.startX;
152
+ el.classList.add('is-dragging');
153
+ };
154
+
155
+ const pointerMove = (e) => {
156
+ if (!state.isDragging) return;
157
+ state.currentX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
158
+ };
159
+
160
+ const pointerUp = () => {
161
+ if (!state.isDragging) return;
162
+ state.isDragging = false;
163
+ el.classList.remove('is-dragging');
164
+ const diff = state.startX - state.currentX;
165
+ if (Math.abs(diff) > state.threshold) {
166
+ if (diff > 0) next();
167
+ else prev();
168
+ }
169
+ };
170
+
171
+ el.addEventListener('mousedown', pointerDown);
172
+ el.addEventListener('mousemove', pointerMove);
173
+ el.addEventListener('mouseup', pointerUp);
174
+ el.addEventListener('mouseleave', pointerUp);
175
+ el.addEventListener('touchstart', pointerDown, { passive: true });
176
+ el.addEventListener('touchmove', pointerMove, { passive: true });
177
+ el.addEventListener('touchend', pointerUp);
178
+
179
+ cleanup.push(
180
+ () => el.removeEventListener('mousedown', pointerDown),
181
+ () => el.removeEventListener('mousemove', pointerMove),
182
+ () => el.removeEventListener('mouseup', pointerUp),
183
+ () => el.removeEventListener('mouseleave', pointerUp),
184
+ () => el.removeEventListener('touchstart', pointerDown),
185
+ () => el.removeEventListener('touchmove', pointerMove),
186
+ () => el.removeEventListener('touchend', pointerUp)
187
+ );
188
+
189
+ // Autoplay
190
+ const startAutoplay = () => {
191
+ stopAutoplay();
192
+ state.autoplayTimer = setInterval(next, interval);
193
+ };
194
+
195
+ const stopAutoplay = () => {
196
+ if (state.autoplayTimer) {
197
+ clearInterval(state.autoplayTimer);
198
+ state.autoplayTimer = null;
199
+ }
200
+ };
201
+
202
+ if (autoplay) {
203
+ startAutoplay();
204
+ const pauseHandler = () => stopAutoplay();
205
+ const resumeHandler = () => startAutoplay();
206
+ el.addEventListener('mouseenter', pauseHandler);
207
+ el.addEventListener('mouseleave', resumeHandler);
208
+ el.addEventListener('focusin', pauseHandler);
209
+ el.addEventListener('focusout', resumeHandler);
210
+ cleanup.push(
211
+ () => el.removeEventListener('mouseenter', pauseHandler),
212
+ () => el.removeEventListener('mouseleave', resumeHandler),
213
+ () => el.removeEventListener('focusin', pauseHandler),
214
+ () => el.removeEventListener('focusout', resumeHandler),
215
+ () => stopAutoplay()
216
+ );
217
+ }
218
+
219
+ // Initial ARIA state
220
+ goTo(0, false);
221
+
222
+ this.instances.set(el, {
223
+ cleanup: cleanup,
224
+ goTo: goTo,
225
+ next: next,
226
+ prev: prev,
227
+ getState: () => ({ ...state })
228
+ });
229
+ },
230
+
231
+ goTo: function (el, index) {
232
+ const instance = this.instances.get(el);
233
+ if (instance) instance.goTo(index);
234
+ },
235
+
236
+ next: function (el) {
237
+ const instance = this.instances.get(el);
238
+ if (instance) instance.next();
239
+ },
240
+
241
+ prev: function (el) {
242
+ const instance = this.instances.get(el);
243
+ if (instance) instance.prev();
244
+ },
245
+
246
+ destroy: function (el) {
247
+ const instance = this.instances.get(el);
248
+ if (!instance) return;
249
+ instance.cleanup.forEach(fn => fn());
250
+ this.instances.delete(el);
251
+ },
252
+
253
+ destroyAll: function () {
254
+ this.instances.forEach((_, el) => this.destroy(el));
255
+ }
256
+ };
257
+
258
+ if (typeof window.Vanduo !== 'undefined') {
259
+ window.Vanduo.register('flow', Flow);
260
+ }
261
+
262
+ window.VanduoFlow = Flow;
263
+
264
+ })();
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Vanduo Framework - Rating Component
3
+ * Star-based rating input with hover preview and read-only mode
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ const Rating = {
10
+ instances: new Map(),
11
+
12
+ init: function () {
13
+ const ratings = document.querySelectorAll('[data-vd-rating]');
14
+ ratings.forEach(el => {
15
+ if (this.instances.has(el)) return;
16
+ this.initInstance(el);
17
+ });
18
+ },
19
+
20
+ initInstance: function (el) {
21
+ const cleanup = [];
22
+ const max = parseInt(el.getAttribute('data-vd-rating-max') || '5', 10);
23
+ const initialValue = parseFloat(el.getAttribute('data-vd-rating-value') || '0');
24
+ const readonly = el.classList.contains('vd-rating-readonly') || el.hasAttribute('data-vd-rating-readonly');
25
+ let currentValue = initialValue;
26
+
27
+ el.classList.add('vd-rating');
28
+ el.setAttribute('role', 'radiogroup');
29
+ el.setAttribute('aria-label', el.getAttribute('aria-label') || 'Rating');
30
+
31
+ // Clear existing stars
32
+ el.innerHTML = '';
33
+
34
+ // Create stars
35
+ const stars = [];
36
+ for (let i = 1; i <= max; i++) {
37
+ const star = document.createElement('button');
38
+ star.type = 'button';
39
+ star.className = 'vd-rating-star';
40
+ star.setAttribute('role', 'radio');
41
+ star.setAttribute('aria-label', i + ' star' + (i > 1 ? 's' : ''));
42
+ star.setAttribute('aria-checked', i <= currentValue ? 'true' : 'false');
43
+ if (readonly) star.tabIndex = -1;
44
+ stars.push(star);
45
+ el.appendChild(star);
46
+ }
47
+
48
+ // Value display
49
+ const valueDisplay = document.createElement('span');
50
+ valueDisplay.className = 'vd-rating-value';
51
+ valueDisplay.textContent = currentValue > 0 ? currentValue.toString() : '';
52
+ el.appendChild(valueDisplay);
53
+
54
+ const updateStars = (value) => {
55
+ stars.forEach((star, i) => {
56
+ star.classList.remove('is-active', 'is-half');
57
+ const starNum = i + 1;
58
+ if (starNum <= Math.floor(value)) {
59
+ star.classList.add('is-active');
60
+ } else if (starNum - 0.5 <= value) {
61
+ star.classList.add('is-half');
62
+ }
63
+ star.setAttribute('aria-checked', starNum <= value ? 'true' : 'false');
64
+ });
65
+ valueDisplay.textContent = value > 0 ? value.toString() : '';
66
+ };
67
+
68
+ updateStars(currentValue);
69
+
70
+ if (!readonly) {
71
+ stars.forEach((star, i) => {
72
+ const enterHandler = () => {
73
+ stars.forEach((s, j) => {
74
+ s.classList.toggle('is-hovered', j <= i);
75
+ });
76
+ };
77
+ const leaveHandler = () => {
78
+ stars.forEach(s => s.classList.remove('is-hovered'));
79
+ };
80
+ const clickHandler = () => {
81
+ currentValue = i + 1;
82
+ el.setAttribute('data-vd-rating-value', currentValue);
83
+ updateStars(currentValue);
84
+ el.dispatchEvent(new CustomEvent('rating:change', {
85
+ detail: { value: currentValue, max },
86
+ bubbles: true
87
+ }));
88
+ };
89
+
90
+ star.addEventListener('mouseenter', enterHandler);
91
+ star.addEventListener('mouseleave', leaveHandler);
92
+ star.addEventListener('click', clickHandler);
93
+
94
+ cleanup.push(
95
+ () => star.removeEventListener('mouseenter', enterHandler),
96
+ () => star.removeEventListener('mouseleave', leaveHandler),
97
+ () => star.removeEventListener('click', clickHandler)
98
+ );
99
+ });
100
+
101
+ // Keyboard
102
+ const keyHandler = (e) => {
103
+ if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
104
+ e.preventDefault();
105
+ if (currentValue < max) {
106
+ currentValue++;
107
+ updateStars(currentValue);
108
+ stars[currentValue - 1].focus();
109
+ el.dispatchEvent(new CustomEvent('rating:change', { detail: { value: currentValue, max }, bubbles: true }));
110
+ }
111
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
112
+ e.preventDefault();
113
+ if (currentValue > 1) {
114
+ currentValue--;
115
+ updateStars(currentValue);
116
+ stars[currentValue - 1].focus();
117
+ el.dispatchEvent(new CustomEvent('rating:change', { detail: { value: currentValue, max }, bubbles: true }));
118
+ }
119
+ }
120
+ };
121
+ el.addEventListener('keydown', keyHandler);
122
+ cleanup.push(() => el.removeEventListener('keydown', keyHandler));
123
+ }
124
+
125
+ this.instances.set(el, {
126
+ cleanup,
127
+ getValue: () => currentValue,
128
+ setValue: (v) => { currentValue = v; updateStars(v); }
129
+ });
130
+ },
131
+
132
+ getValue: function (el) {
133
+ const inst = this.instances.get(el);
134
+ return inst ? inst.getValue() : 0;
135
+ },
136
+
137
+ setValue: function (el, value) {
138
+ const inst = this.instances.get(el);
139
+ if (inst) inst.setValue(value);
140
+ },
141
+
142
+ destroy: function (el) {
143
+ const inst = this.instances.get(el);
144
+ if (!inst) return;
145
+ inst.cleanup.forEach(fn => fn());
146
+ this.instances.delete(el);
147
+ },
148
+
149
+ destroyAll: function () {
150
+ this.instances.forEach((_, el) => this.destroy(el));
151
+ }
152
+ };
153
+
154
+ if (typeof window.Vanduo !== 'undefined') {
155
+ window.Vanduo.register('rating', Rating);
156
+ }
157
+
158
+ window.VanduoRating = Rating;
159
+
160
+ })();
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Vanduo Framework - Ripple (Waves Effect) Component
3
+ * Adds expanding circle animation on click at pointer position
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ const Ripple = {
10
+ instances: new Map(),
11
+
12
+ init: function () {
13
+ const elements = document.querySelectorAll('.vd-ripple, [data-vd-ripple]');
14
+ elements.forEach(el => {
15
+ if (this.instances.has(el)) return;
16
+ this.initInstance(el);
17
+ });
18
+ },
19
+
20
+ initInstance: function (el) {
21
+ const cleanup = [];
22
+
23
+ const createWave = (e) => {
24
+ const rect = el.getBoundingClientRect();
25
+ const size = Math.max(rect.width, rect.height);
26
+ const x = (e.clientX || (e.touches && e.touches[0].clientX) || rect.left + rect.width / 2) - rect.left - size / 2;
27
+ const y = (e.clientY || (e.touches && e.touches[0].clientY) || rect.top + rect.height / 2) - rect.top - size / 2;
28
+
29
+ const wave = document.createElement('span');
30
+ wave.className = 'vd-ripple-wave';
31
+ wave.style.width = size + 'px';
32
+ wave.style.height = size + 'px';
33
+ wave.style.left = x + 'px';
34
+ wave.style.top = y + 'px';
35
+
36
+ el.appendChild(wave);
37
+
38
+ wave.addEventListener('animationend', () => {
39
+ if (wave.parentNode) wave.parentNode.removeChild(wave);
40
+ });
41
+ };
42
+
43
+ el.addEventListener('mousedown', createWave);
44
+ el.addEventListener('touchstart', createWave, { passive: true });
45
+
46
+ cleanup.push(
47
+ () => el.removeEventListener('mousedown', createWave),
48
+ () => el.removeEventListener('touchstart', createWave)
49
+ );
50
+
51
+ this.instances.set(el, { cleanup });
52
+ },
53
+
54
+ destroy: function (el) {
55
+ const instance = this.instances.get(el);
56
+ if (!instance) return;
57
+ instance.cleanup.forEach(fn => fn());
58
+ // Remove any lingering wave elements
59
+ el.querySelectorAll('.vd-ripple-wave').forEach(w => w.remove());
60
+ this.instances.delete(el);
61
+ },
62
+
63
+ destroyAll: function () {
64
+ this.instances.forEach((_, el) => this.destroy(el));
65
+ }
66
+ };
67
+
68
+ if (typeof window.Vanduo !== 'undefined') {
69
+ window.Vanduo.register('ripple', Ripple);
70
+ }
71
+
72
+ window.VanduoRipple = Ripple;
73
+
74
+ })();
@@ -32,7 +32,7 @@
32
32
  * Initialize sidenav components
33
33
  */
34
34
  init: function() {
35
- const sidenavs = document.querySelectorAll('.vd-sidenav');
35
+ const sidenavs = document.querySelectorAll('.vd-sidenav, .vd-offcanvas');
36
36
 
37
37
  sidenavs.forEach(sidenav => {
38
38
  if (this.sidenavs.has(sidenav)) {
@@ -73,8 +73,15 @@
73
73
  * @param {HTMLElement} sidenav - Sidenav element
74
74
  */
75
75
  initSidenav: function(sidenav) {
76
+ // Apply data-vd-position direction class if specified
77
+ const position = sidenav.getAttribute('data-vd-position');
78
+ if (position) {
79
+ const prefix = sidenav.classList.contains('vd-offcanvas') ? 'vd-offcanvas' : 'vd-sidenav';
80
+ sidenav.classList.add(prefix + '-' + position);
81
+ }
82
+
76
83
  const overlay = this.createOverlay(sidenav);
77
- const closeButton = sidenav.querySelector('.vd-sidenav-close');
84
+ const closeButton = sidenav.querySelector('.vd-sidenav-close, .vd-offcanvas-close');
78
85
  const cleanupFunctions = [];
79
86
 
80
87
  // Set ARIA attributes