@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,185 @@
1
+ /**
2
+ * Vanduo Framework - Validate (Form Validation) Component
3
+ * Declarative validation via data attributes with real-time and on-submit modes
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ const Validate = {
10
+ instances: new Map(),
11
+
12
+ rules: {
13
+ required: (value) => value.trim().length > 0,
14
+ email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
15
+ url: (value) => { try { new URL(value); return true; } catch (_e) { return false; } },
16
+ number: (value) => !isNaN(parseFloat(value)) && isFinite(value),
17
+ min: (value, param) => value.length >= parseInt(param, 10),
18
+ max: (value, param) => value.length <= parseInt(param, 10),
19
+ minVal: (value, param) => parseFloat(value) >= parseFloat(param),
20
+ maxVal: (value, param) => parseFloat(value) <= parseFloat(param),
21
+ pattern: (value, param) => { try { return new RegExp(param).test(value); } catch (_e) { return false; } },
22
+ match: (value, param) => {
23
+ const other = document.querySelector('[name="' + param + '"]');
24
+ return other ? value === other.value : false;
25
+ }
26
+ },
27
+
28
+ messages: {
29
+ required: 'This field is required',
30
+ email: 'Please enter a valid email address',
31
+ url: 'Please enter a valid URL',
32
+ number: 'Please enter a valid number',
33
+ min: 'Minimum {0} characters required',
34
+ max: 'Maximum {0} characters allowed',
35
+ minVal: 'Value must be at least {0}',
36
+ maxVal: 'Value must be at most {0}',
37
+ pattern: 'Invalid format',
38
+ match: 'Fields do not match'
39
+ },
40
+
41
+ init: function () {
42
+ const forms = document.querySelectorAll('[data-vd-validate], .vd-validate');
43
+ forms.forEach(form => {
44
+ if (this.instances.has(form)) return;
45
+ this.initInstance(form);
46
+ });
47
+ },
48
+
49
+ initInstance: function (form) {
50
+ const cleanup = [];
51
+ const mode = form.getAttribute('data-vd-validate-mode') || 'blur'; // blur | input | submit
52
+ const fields = form.querySelectorAll('[data-vd-rules]');
53
+
54
+ const validateField = (field) => {
55
+ const rulesStr = field.getAttribute('data-vd-rules') || '';
56
+ const rules = rulesStr.split('|').map(r => r.trim()).filter(Boolean);
57
+ const value = field.value;
58
+ const errors = [];
59
+
60
+ for (const rule of rules) {
61
+ const [name, ...params] = rule.split(':');
62
+ const param = params.join(':');
63
+ const validator = this.rules[name];
64
+
65
+ if (validator && !validator(value, param)) {
66
+ const customMsg = field.getAttribute('data-vd-msg-' + name);
67
+ let msg = customMsg || this.messages[name] || 'Invalid';
68
+ if (param) msg = msg.replace('{0}', param);
69
+ errors.push(msg);
70
+ break; // one error at a time
71
+ }
72
+ }
73
+
74
+ this.setFieldState(field, errors);
75
+ return errors.length === 0;
76
+ };
77
+
78
+ const validateAll = () => {
79
+ let valid = true;
80
+ fields.forEach(field => {
81
+ if (!validateField(field)) valid = false;
82
+ });
83
+ return valid;
84
+ };
85
+
86
+ // Per-field listeners
87
+ fields.forEach(field => {
88
+ if (mode === 'input' || mode === 'blur') {
89
+ const eventType = mode === 'input' ? 'input' : 'blur';
90
+ const handler = () => validateField(field);
91
+ field.addEventListener(eventType, handler);
92
+ cleanup.push(() => field.removeEventListener(eventType, handler));
93
+
94
+ if (mode === 'blur') {
95
+ const inputClear = () => {
96
+ if (field.classList.contains('is-invalid') || field.classList.contains('is-valid')) {
97
+ validateField(field);
98
+ }
99
+ };
100
+ field.addEventListener('input', inputClear);
101
+ cleanup.push(() => field.removeEventListener('input', inputClear));
102
+ }
103
+ }
104
+ });
105
+
106
+ // Form submit
107
+ const submitHandler = (e) => {
108
+ const valid = validateAll();
109
+ if (!valid) {
110
+ e.preventDefault();
111
+ e.stopPropagation();
112
+ // Focus first invalid field
113
+ const firstInvalid = form.querySelector('.is-invalid');
114
+ if (firstInvalid) firstInvalid.focus();
115
+ }
116
+ form.dispatchEvent(new CustomEvent('validate:submit', {
117
+ detail: { valid },
118
+ bubbles: true
119
+ }));
120
+ };
121
+
122
+ form.addEventListener('submit', submitHandler);
123
+ cleanup.push(() => form.removeEventListener('submit', submitHandler));
124
+
125
+ this.instances.set(form, { cleanup, validateAll, validateField });
126
+ },
127
+
128
+ setFieldState: function (field, errors) {
129
+ const wrapper = field.closest('.vd-form-group') || field.parentElement;
130
+ let errorEl = wrapper.querySelector('.vd-validate-error');
131
+
132
+ field.classList.remove('is-valid', 'is-invalid');
133
+
134
+ if (errors.length > 0) {
135
+ field.classList.add('is-invalid');
136
+ field.setAttribute('aria-invalid', 'true');
137
+ if (!errorEl) {
138
+ errorEl = document.createElement('div');
139
+ errorEl.className = 'vd-validate-error';
140
+ errorEl.id = 'vd-err-' + Math.random().toString(36).slice(2, 9);
141
+ errorEl.setAttribute('role', 'alert');
142
+ wrapper.appendChild(errorEl);
143
+ }
144
+ errorEl.textContent = errors[0];
145
+ errorEl.style.display = '';
146
+ field.setAttribute('aria-describedby', errorEl.id);
147
+ } else if (field.value.trim()) {
148
+ field.classList.add('is-valid');
149
+ field.removeAttribute('aria-invalid');
150
+ if (errorEl) errorEl.style.display = 'none';
151
+ } else {
152
+ field.removeAttribute('aria-invalid');
153
+ if (errorEl) errorEl.style.display = 'none';
154
+ }
155
+ },
156
+
157
+ validateForm: function (form) {
158
+ const instance = this.instances.get(form);
159
+ return instance ? instance.validateAll() : false;
160
+ },
161
+
162
+ addRule: function (name, validator, message) {
163
+ this.rules[name] = validator;
164
+ if (message) this.messages[name] = message;
165
+ },
166
+
167
+ destroy: function (form) {
168
+ const instance = this.instances.get(form);
169
+ if (!instance) return;
170
+ instance.cleanup.forEach(fn => fn());
171
+ this.instances.delete(form);
172
+ },
173
+
174
+ destroyAll: function () {
175
+ this.instances.forEach((_, form) => this.destroy(form));
176
+ }
177
+ };
178
+
179
+ if (typeof window.Vanduo !== 'undefined') {
180
+ window.Vanduo.register('validate', Validate);
181
+ }
182
+
183
+ window.VanduoValidate = Validate;
184
+
185
+ })();
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Vanduo Framework - Waypoint (Scrollspy) Component
3
+ * Highlights navigation links based on scroll position using IntersectionObserver
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ const Waypoint = {
10
+ instances: new Map(),
11
+
12
+ init: function () {
13
+ const navs = document.querySelectorAll('[data-vd-waypoint-nav], [data-vd-scrollspy-nav]');
14
+ navs.forEach(nav => {
15
+ if (this.instances.has(nav)) return;
16
+ this.initInstance(nav);
17
+ });
18
+ },
19
+
20
+ initInstance: function (nav) {
21
+ const links = Array.from(nav.querySelectorAll('a[href^="#"]'));
22
+ if (links.length === 0) return;
23
+
24
+ const cleanup = [];
25
+ const offset = parseInt(nav.getAttribute('data-vd-waypoint-offset') || '80', 10);
26
+ const sections = [];
27
+
28
+ links.forEach(link => {
29
+ const id = link.getAttribute('href').slice(1);
30
+ const section = document.getElementById(id);
31
+ if (section) {
32
+ section.setAttribute('data-vd-waypoint-section', '');
33
+ sections.push({ id, link, section });
34
+ }
35
+ });
36
+
37
+ if (sections.length === 0) return;
38
+
39
+ const activeSections = new Set();
40
+
41
+ const setActive = (id) => {
42
+ links.forEach(l => l.classList.remove('is-active'));
43
+ const target = links.find(l => l.getAttribute('href') === '#' + id);
44
+ if (target) {
45
+ target.classList.add('is-active');
46
+ nav.dispatchEvent(new CustomEvent('waypoint:change', {
47
+ detail: { activeId: id, link: target }
48
+ }));
49
+ }
50
+ };
51
+
52
+ const rootMargin = '-' + offset + 'px 0px -40% 0px';
53
+
54
+ const observer = new IntersectionObserver((entries) => {
55
+ entries.forEach(entry => {
56
+ if (entry.isIntersecting) {
57
+ activeSections.add(entry.target.id);
58
+ } else {
59
+ activeSections.delete(entry.target.id);
60
+ }
61
+ });
62
+
63
+ // Pick the topmost visible section
64
+ for (let i = 0; i < sections.length; i++) {
65
+ if (activeSections.has(sections[i].id)) {
66
+ setActive(sections[i].id);
67
+ return;
68
+ }
69
+ }
70
+ }, {
71
+ rootMargin: rootMargin,
72
+ threshold: 0
73
+ });
74
+
75
+ sections.forEach(s => observer.observe(s.section));
76
+
77
+ // Smooth scroll on click
78
+ links.forEach(link => {
79
+ const clickHandler = (e) => {
80
+ e.preventDefault();
81
+ const id = link.getAttribute('href').slice(1);
82
+ const section = document.getElementById(id);
83
+ if (section) {
84
+ section.scrollIntoView({ behavior: 'smooth' });
85
+ setActive(id);
86
+ }
87
+ };
88
+ link.addEventListener('click', clickHandler);
89
+ cleanup.push(() => link.removeEventListener('click', clickHandler));
90
+ });
91
+
92
+ cleanup.push(() => observer.disconnect());
93
+
94
+ this.instances.set(nav, { observer, cleanup, sections, setActive });
95
+ },
96
+
97
+ refresh: function (nav) {
98
+ this.destroy(nav);
99
+ this.initInstance(nav);
100
+ },
101
+
102
+ destroy: function (nav) {
103
+ const instance = this.instances.get(nav);
104
+ if (!instance) return;
105
+ instance.cleanup.forEach(fn => fn());
106
+ this.instances.delete(nav);
107
+ },
108
+
109
+ destroyAll: function () {
110
+ this.instances.forEach((_, nav) => this.destroy(nav));
111
+ }
112
+ };
113
+
114
+ if (typeof window.Vanduo !== 'undefined') {
115
+ window.Vanduo.register('waypoint', Waypoint);
116
+ }
117
+
118
+ window.VanduoWaypoint = Waypoint;
119
+
120
+ })();
package/js/index.js CHANGED
@@ -47,6 +47,22 @@ import './components/doc-search.js';
47
47
  import './components/draggable.js';
48
48
  import './components/lazy-load.js';
49
49
 
50
+ // Phase 10 (v1.2.7) components
51
+ import './components/flow.js';
52
+ import './components/bubble.js';
53
+ import './components/waypoint.js';
54
+ import './components/ripple.js';
55
+ import './components/affix.js';
56
+ import './components/suggest.js';
57
+ import './components/validate.js';
58
+ import './components/datepicker.js';
59
+ import './components/timepicker.js';
60
+ import './components/stepper.js';
61
+ import './components/rating.js';
62
+ import './components/transfer.js';
63
+ import './components/tree.js';
64
+ import './components/spotlight.js';
65
+
50
66
  // Re-export for ESM / CJS consumers
51
67
  const Vanduo = window.Vanduo;
52
68
  export { Vanduo };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanduo-oss/framework",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
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",
@@ -41,10 +41,10 @@
41
41
  "devDependencies": {
42
42
  "@eslint/js": "^10.0.1",
43
43
  "@playwright/test": "^1.58.2",
44
- "esbuild": "^0.27.3",
45
- "eslint": "^10.0.2",
44
+ "esbuild": "^0.27.4",
45
+ "eslint": "^10.0.3",
46
46
  "husky": "^9.1.7",
47
- "lightningcss": "^1.31.1",
47
+ "lightningcss": "^1.32.0",
48
48
  "stylelint": "^17.4.0",
49
49
  "stylelint-config-standard": "^40.0.0"
50
50
  },