@vanduo-oss/framework 1.2.6 → 1.2.8

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 (54) hide show
  1. package/README.md +39 -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 +2161 -6
  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 +1947 -5
  25. package/dist/vanduo.css.map +1 -1
  26. package/dist/vanduo.esm.js +2161 -6
  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 +2161 -6
  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/theme-customizer.js +11 -2
  47. package/js/components/theme-switcher.js +7 -0
  48. package/js/components/timepicker.js +142 -0
  49. package/js/components/transfer.js +206 -0
  50. package/js/components/tree.js +191 -0
  51. package/js/components/validate.js +185 -0
  52. package/js/components/waypoint.js +120 -0
  53. package/js/index.js +16 -0
  54. package/package.json +4 -4
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Vanduo Framework - Timepicker Component
3
+ * Dropdown time selection with 12h/24h format and configurable step intervals
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ const Timepicker = {
10
+ instances: new Map(),
11
+
12
+ init: function () {
13
+ const inputs = document.querySelectorAll('[data-vd-timepicker]');
14
+ inputs.forEach(el => {
15
+ if (this.instances.has(el)) return;
16
+ this.initInstance(el);
17
+ });
18
+ },
19
+
20
+ initInstance: function (input) {
21
+ const cleanup = [];
22
+ const is24h = input.getAttribute('data-vd-timepicker-format') === '24h';
23
+ const step = parseInt(input.getAttribute('data-vd-timepicker-step') || '30', 10);
24
+
25
+ // Create wrapper
26
+ let wrapper = input.closest('.vd-suggest-wrapper');
27
+ if (!wrapper) {
28
+ wrapper = document.createElement('div');
29
+ wrapper.style.position = 'relative';
30
+ wrapper.style.display = 'inline-block';
31
+ input.parentNode.insertBefore(wrapper, input);
32
+ wrapper.appendChild(input);
33
+ }
34
+
35
+ // Create popup
36
+ const popup = document.createElement('div');
37
+ popup.className = 'vd-timepicker-popup';
38
+ popup.setAttribute('role', 'listbox');
39
+ wrapper.appendChild(popup);
40
+
41
+ // Generate time slots
42
+ const times = [];
43
+ for (let h = 0; h < 24; h++) {
44
+ for (let m = 0; m < 60; m += step) {
45
+ const hh24 = String(h).padStart(2, '0');
46
+ const mm = String(m).padStart(2, '0');
47
+
48
+ if (is24h) {
49
+ times.push({ display: hh24 + ':' + mm, value: hh24 + ':' + mm });
50
+ } else {
51
+ const period = h < 12 ? 'AM' : 'PM';
52
+ const h12 = h === 0 ? 12 : (h > 12 ? h - 12 : h);
53
+ const display = h12 + ':' + mm + ' ' + period;
54
+ times.push({ display, value: hh24 + ':' + mm });
55
+ }
56
+ }
57
+ }
58
+
59
+ const render = () => {
60
+ popup.innerHTML = '';
61
+ times.forEach(t => {
62
+ const item = document.createElement('div');
63
+ item.className = 'vd-timepicker-item';
64
+ item.setAttribute('role', 'option');
65
+ item.textContent = t.display;
66
+
67
+ if (input.value === t.value || input.value === t.display) {
68
+ item.classList.add('is-selected');
69
+ }
70
+
71
+ item.addEventListener('click', () => {
72
+ input.value = t.display;
73
+ popup.querySelectorAll('.vd-timepicker-item').forEach(i => i.classList.remove('is-selected'));
74
+ item.classList.add('is-selected');
75
+ close();
76
+ input.dispatchEvent(new CustomEvent('timepicker:select', {
77
+ detail: { display: t.display, value: t.value },
78
+ bubbles: true
79
+ }));
80
+ input.dispatchEvent(new Event('change', { bubbles: true }));
81
+ });
82
+
83
+ popup.appendChild(item);
84
+ });
85
+ };
86
+
87
+ const open = () => {
88
+ render();
89
+ popup.classList.add('is-open');
90
+ input.setAttribute('aria-expanded', 'true');
91
+ // Scroll to selected
92
+ const selected = popup.querySelector('.is-selected');
93
+ if (selected) selected.scrollIntoView({ block: 'center' });
94
+ };
95
+
96
+ const close = () => {
97
+ popup.classList.remove('is-open');
98
+ input.setAttribute('aria-expanded', 'false');
99
+ };
100
+
101
+ const focusHandler = () => open();
102
+ const outsideHandler = (e) => {
103
+ if (!wrapper.contains(e.target)) close();
104
+ };
105
+ const escHandler = (e) => { if (e.key === 'Escape') close(); };
106
+
107
+ input.addEventListener('focus', focusHandler);
108
+ document.addEventListener('click', outsideHandler, true);
109
+ document.addEventListener('keydown', escHandler);
110
+ input.setAttribute('aria-haspopup', 'listbox');
111
+ input.setAttribute('aria-expanded', 'false');
112
+ input.setAttribute('autocomplete', 'off');
113
+ input.readOnly = true;
114
+
115
+ cleanup.push(
116
+ () => input.removeEventListener('focus', focusHandler),
117
+ () => document.removeEventListener('click', outsideHandler, true),
118
+ () => document.removeEventListener('keydown', escHandler)
119
+ );
120
+
121
+ this.instances.set(input, { cleanup, open, close });
122
+ },
123
+
124
+ destroy: function (el) {
125
+ const instance = this.instances.get(el);
126
+ if (!instance) return;
127
+ instance.cleanup.forEach(fn => fn());
128
+ this.instances.delete(el);
129
+ },
130
+
131
+ destroyAll: function () {
132
+ this.instances.forEach((_, el) => this.destroy(el));
133
+ }
134
+ };
135
+
136
+ if (typeof window.Vanduo !== 'undefined') {
137
+ window.Vanduo.register('timepicker', Timepicker);
138
+ }
139
+
140
+ window.VanduoTimepicker = Timepicker;
141
+
142
+ })();
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Vanduo Framework - Transfer (Dual-list) Component
3
+ * Source-to-target list transfer with search, select-all, and move actions
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ const Transfer = {
10
+ instances: new Map(),
11
+
12
+ init: function () {
13
+ const transfers = document.querySelectorAll('[data-vd-transfer]');
14
+ transfers.forEach(el => {
15
+ if (this.instances.has(el)) return;
16
+ this.initInstance(el);
17
+ });
18
+ },
19
+
20
+ initInstance: function (el) {
21
+ const cleanup = [];
22
+ el.classList.add('vd-transfer');
23
+
24
+ let sourceData, targetData;
25
+ try {
26
+ const raw = JSON.parse(el.getAttribute('data-vd-transfer') || '[]');
27
+ sourceData = raw.map((item, i) => ({
28
+ id: item.id || 'item-' + i,
29
+ label: item.label || item.text || String(item),
30
+ selected: false
31
+ }));
32
+ } catch (_e) {
33
+ sourceData = [];
34
+ }
35
+ targetData = [];
36
+
37
+ const sourceSelected = new Set();
38
+ const targetSelected = new Set();
39
+
40
+ const render = () => {
41
+ el.innerHTML = '';
42
+
43
+ // Source panel
44
+ const sourcePanel = createPanel('Source', sourceData, sourceSelected, 'source');
45
+ // Actions
46
+ const actions = document.createElement('div');
47
+ actions.className = 'vd-transfer-actions';
48
+
49
+ const moveRightBtn = document.createElement('button');
50
+ moveRightBtn.type = 'button';
51
+ moveRightBtn.className = 'vd-transfer-btn';
52
+ moveRightBtn.innerHTML = '&#8250;';
53
+ moveRightBtn.setAttribute('aria-label', 'Move to target');
54
+ moveRightBtn.disabled = sourceSelected.size === 0;
55
+ moveRightBtn.addEventListener('click', () => moveRight());
56
+
57
+ const moveLeftBtn = document.createElement('button');
58
+ moveLeftBtn.type = 'button';
59
+ moveLeftBtn.className = 'vd-transfer-btn';
60
+ moveLeftBtn.innerHTML = '&#8249;';
61
+ moveLeftBtn.setAttribute('aria-label', 'Move to source');
62
+ moveLeftBtn.disabled = targetSelected.size === 0;
63
+ moveLeftBtn.addEventListener('click', () => moveLeft());
64
+
65
+ actions.appendChild(moveRightBtn);
66
+ actions.appendChild(moveLeftBtn);
67
+
68
+ // Target panel
69
+ const targetPanel = createPanel('Target', targetData, targetSelected, 'target');
70
+
71
+ el.appendChild(sourcePanel);
72
+ el.appendChild(actions);
73
+ el.appendChild(targetPanel);
74
+ };
75
+
76
+ const createPanel = (title, data, selected, _side) => {
77
+ const panel = document.createElement('div');
78
+ panel.className = 'vd-transfer-panel';
79
+
80
+ const header = document.createElement('div');
81
+ header.className = 'vd-transfer-header';
82
+ const titleSpan = document.createElement('span');
83
+ titleSpan.textContent = title;
84
+ const count = document.createElement('span');
85
+ count.className = 'vd-transfer-count';
86
+ count.textContent = selected.size + '/' + data.length;
87
+ header.appendChild(titleSpan);
88
+ header.appendChild(count);
89
+ panel.appendChild(header);
90
+
91
+ // Search
92
+ const searchDiv = document.createElement('div');
93
+ searchDiv.className = 'vd-transfer-search';
94
+ const searchInput = document.createElement('input');
95
+ searchInput.type = 'text';
96
+ searchInput.placeholder = 'Search...';
97
+ searchInput.setAttribute('aria-label', 'Search ' + title.toLowerCase());
98
+ searchDiv.appendChild(searchInput);
99
+ panel.appendChild(searchDiv);
100
+
101
+ // List
102
+ const list = document.createElement('ul');
103
+ list.className = 'vd-transfer-list';
104
+ list.setAttribute('role', 'listbox');
105
+
106
+ const renderList = (filter) => {
107
+ list.innerHTML = '';
108
+ const filtered = filter ? data.filter(d => {
109
+ const label = (d.label || d.text || String(d)).toLowerCase();
110
+ return label.includes(filter.toLowerCase());
111
+ }) : data;
112
+ filtered.forEach(item => {
113
+ const li = document.createElement('li');
114
+ li.className = 'vd-transfer-item';
115
+ li.setAttribute('role', 'option');
116
+ if (selected.has(item.id)) li.classList.add('is-selected');
117
+
118
+ const checkbox = document.createElement('input');
119
+ checkbox.type = 'checkbox';
120
+ checkbox.checked = selected.has(item.id);
121
+ checkbox.setAttribute('aria-label', item.label);
122
+
123
+ const label = document.createElement('span');
124
+ label.textContent = item.label;
125
+
126
+ li.addEventListener('click', () => {
127
+ if (selected.has(item.id)) selected.delete(item.id);
128
+ else selected.add(item.id);
129
+ render();
130
+ });
131
+
132
+ li.appendChild(checkbox);
133
+ li.appendChild(label);
134
+ list.appendChild(li);
135
+ });
136
+ };
137
+
138
+ searchInput.addEventListener('input', () => renderList(searchInput.value));
139
+ renderList('');
140
+
141
+ panel.appendChild(list);
142
+ return panel;
143
+ };
144
+
145
+ const moveRight = () => {
146
+ const toMove = sourceData.filter(d => sourceSelected.has(d.id));
147
+ sourceData = sourceData.filter(d => !sourceSelected.has(d.id));
148
+ targetData = targetData.concat(toMove);
149
+ sourceSelected.clear();
150
+ render();
151
+ fireChange();
152
+ };
153
+
154
+ const moveLeft = () => {
155
+ const toMove = targetData.filter(d => targetSelected.has(d.id));
156
+ targetData = targetData.filter(d => !targetSelected.has(d.id));
157
+ sourceData = sourceData.concat(toMove);
158
+ targetSelected.clear();
159
+ render();
160
+ fireChange();
161
+ };
162
+
163
+ const fireChange = () => {
164
+ el.dispatchEvent(new CustomEvent('transfer:change', {
165
+ detail: {
166
+ source: sourceData.map(d => d.id),
167
+ target: targetData.map(d => d.id)
168
+ },
169
+ bubbles: true
170
+ }));
171
+ };
172
+
173
+ render();
174
+
175
+ this.instances.set(el, {
176
+ cleanup,
177
+ getTarget: () => targetData.map(d => d.id),
178
+ getSource: () => sourceData.map(d => d.id)
179
+ });
180
+ },
181
+
182
+ getSelected: function (el) {
183
+ const inst = this.instances.get(el);
184
+ return inst ? inst.getTarget() : [];
185
+ },
186
+
187
+ destroy: function (el) {
188
+ const inst = this.instances.get(el);
189
+ if (!inst) return;
190
+ inst.cleanup.forEach(fn => fn());
191
+ el.innerHTML = '';
192
+ this.instances.delete(el);
193
+ },
194
+
195
+ destroyAll: function () {
196
+ this.instances.forEach((_, el) => this.destroy(el));
197
+ }
198
+ };
199
+
200
+ if (typeof window.Vanduo !== 'undefined') {
201
+ window.Vanduo.register('transfer', Transfer);
202
+ }
203
+
204
+ window.VanduoTransfer = Transfer;
205
+
206
+ })();
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Vanduo Framework - Tree View Component
3
+ * Hierarchical collapsible tree with checkbox selection and keyboard navigation
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ const Tree = {
10
+ instances: new Map(),
11
+
12
+ init: function () {
13
+ const trees = document.querySelectorAll('[data-vd-tree]');
14
+ trees.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 cascade = el.getAttribute('data-vd-tree-cascade') !== 'false';
23
+
24
+ let data;
25
+ try { data = JSON.parse(el.getAttribute('data-vd-tree') || '[]'); } catch (_e) { data = []; }
26
+
27
+ el.classList.add('vd-tree');
28
+ el.setAttribute('role', 'tree');
29
+
30
+ const render = (items, parent) => {
31
+ parent.innerHTML = '';
32
+ items.forEach(item => {
33
+ const node = document.createElement('li');
34
+ node.className = 'vd-tree-node';
35
+ node.setAttribute('role', 'treeitem');
36
+ node.setAttribute('aria-expanded', item.open ? 'true' : 'false');
37
+ if (item.open) node.classList.add('is-open');
38
+
39
+ const content = document.createElement('div');
40
+ content.className = 'vd-tree-node-content';
41
+
42
+ // Toggle
43
+ if (item.children && item.children.length > 0) {
44
+ const toggle = document.createElement('button');
45
+ toggle.type = 'button';
46
+ toggle.className = 'vd-tree-toggle';
47
+ toggle.setAttribute('aria-label', 'Toggle');
48
+ toggle.addEventListener('click', (e) => {
49
+ e.stopPropagation();
50
+ item.open = !item.open;
51
+ node.classList.toggle('is-open');
52
+ node.setAttribute('aria-expanded', item.open ? 'true' : 'false');
53
+ });
54
+ content.appendChild(toggle);
55
+ } else {
56
+ const ph = document.createElement('span');
57
+ ph.className = 'vd-tree-toggle-placeholder';
58
+ content.appendChild(ph);
59
+ }
60
+
61
+ // Checkbox
62
+ if (el.hasAttribute('data-vd-tree-checkbox')) {
63
+ const cb = document.createElement('input');
64
+ cb.type = 'checkbox';
65
+ cb.className = 'vd-tree-checkbox';
66
+ cb.checked = !!item.checked;
67
+ cb.setAttribute('aria-label', item.label);
68
+ cb.addEventListener('change', (e) => {
69
+ e.stopPropagation();
70
+ item.checked = cb.checked;
71
+ if (cascade && item.children) {
72
+ setChildChecked(item.children, cb.checked);
73
+ render(data, el);
74
+ }
75
+ el.dispatchEvent(new CustomEvent('tree:check', {
76
+ detail: { id: item.id, checked: cb.checked, label: item.label },
77
+ bubbles: true
78
+ }));
79
+ });
80
+ content.appendChild(cb);
81
+ }
82
+
83
+ // Icon
84
+ if (item.icon) {
85
+ const icon = document.createElement('span');
86
+ icon.className = 'vd-tree-icon ' + item.icon;
87
+ content.appendChild(icon);
88
+ }
89
+
90
+ // Label
91
+ const label = document.createElement('span');
92
+ label.className = 'vd-tree-label';
93
+ label.textContent = item.label || '';
94
+ content.appendChild(label);
95
+
96
+ node.appendChild(content);
97
+
98
+ // Children
99
+ if (item.children && item.children.length > 0) {
100
+ const childList = document.createElement('ul');
101
+ childList.className = 'vd-tree-children';
102
+ childList.setAttribute('role', 'group');
103
+ render(item.children, childList);
104
+ node.appendChild(childList);
105
+ }
106
+
107
+ parent.appendChild(node);
108
+ });
109
+ };
110
+
111
+ const setChildChecked = (items, checked) => {
112
+ items.forEach(item => {
113
+ item.checked = checked;
114
+ if (item.children) setChildChecked(item.children, checked);
115
+ });
116
+ };
117
+
118
+ // Keyboard
119
+ const keyHandler = (e) => {
120
+ const focused = document.activeElement;
121
+ if (!el.contains(focused)) return;
122
+
123
+ const nodes = Array.from(el.querySelectorAll('.vd-tree-node-content'));
124
+ const idx = nodes.indexOf(focused.closest('.vd-tree-node-content'));
125
+ if (idx === -1) return;
126
+
127
+ switch (e.key) {
128
+ case 'ArrowDown':
129
+ e.preventDefault();
130
+ if (idx < nodes.length - 1) {
131
+ const next = nodes[idx + 1].querySelector('.vd-tree-toggle, .vd-tree-label');
132
+ if (next) next.focus();
133
+ }
134
+ break;
135
+ case 'ArrowUp':
136
+ e.preventDefault();
137
+ if (idx > 0) {
138
+ const prev = nodes[idx - 1].querySelector('.vd-tree-toggle, .vd-tree-label');
139
+ if (prev) prev.focus();
140
+ }
141
+ break;
142
+ }
143
+ };
144
+
145
+ el.addEventListener('keydown', keyHandler);
146
+ cleanup.push(() => el.removeEventListener('keydown', keyHandler));
147
+
148
+ render(data, el);
149
+
150
+ this.instances.set(el, {
151
+ cleanup,
152
+ getData: () => data,
153
+ getChecked: () => {
154
+ const checked = [];
155
+ const collect = (items) => {
156
+ items.forEach(i => {
157
+ if (i.checked) checked.push(i.id || i.label);
158
+ if (i.children) collect(i.children);
159
+ });
160
+ };
161
+ collect(data);
162
+ return checked;
163
+ }
164
+ });
165
+ },
166
+
167
+ getChecked: function (el) {
168
+ const inst = this.instances.get(el);
169
+ return inst ? inst.getChecked() : [];
170
+ },
171
+
172
+ destroy: function (el) {
173
+ const inst = this.instances.get(el);
174
+ if (!inst) return;
175
+ inst.cleanup.forEach(fn => fn());
176
+ el.innerHTML = '';
177
+ this.instances.delete(el);
178
+ },
179
+
180
+ destroyAll: function () {
181
+ this.instances.forEach((_, el) => this.destroy(el));
182
+ }
183
+ };
184
+
185
+ if (typeof window.Vanduo !== 'undefined') {
186
+ window.Vanduo.register('tree', Tree);
187
+ }
188
+
189
+ window.VanduoTree = Tree;
190
+
191
+ })();