@vanduo-oss/framework 1.3.9 → 1.4.0

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 (119) hide show
  1. package/README.md +87 -42
  2. package/css/components/affix.css +1 -1
  3. package/css/components/alerts.css +40 -40
  4. package/css/components/avatar.css +33 -33
  5. package/css/components/badges.css +42 -42
  6. package/css/components/breadcrumbs.css +5 -5
  7. package/css/components/bubble.css +4 -4
  8. package/css/components/buttons.css +124 -124
  9. package/css/components/cards.css +10 -10
  10. package/css/components/chips.css +28 -28
  11. package/css/components/code-snippet.css +18 -18
  12. package/css/components/collapsible.css +20 -20
  13. package/css/components/collections.css +21 -21
  14. package/css/components/datepicker.css +13 -13
  15. package/css/components/doc-search.css +46 -53
  16. package/css/components/doc-tabs.css +10 -10
  17. package/css/components/draggable.css +34 -34
  18. package/css/components/dropdown.css +14 -14
  19. package/css/components/expanding-cards.css +1 -1
  20. package/css/components/fab.css +7 -7
  21. package/css/components/flow.css +3 -3
  22. package/css/components/footer.css +26 -26
  23. package/css/components/forms.css +95 -83
  24. package/css/components/image-box.css +13 -17
  25. package/css/components/modals.css +8 -8
  26. package/css/components/music-player.css +26 -26
  27. package/css/components/navbar.css +27 -27
  28. package/css/components/pagination.css +15 -15
  29. package/css/components/preloader.css +10 -10
  30. package/css/components/progress.css +8 -8
  31. package/css/components/rating.css +4 -4
  32. package/css/components/sidenav.css +14 -14
  33. package/css/components/skeleton.css +10 -9
  34. package/css/components/spinner.css +10 -10
  35. package/css/components/spotlight.css +7 -7
  36. package/css/components/stepper.css +13 -13
  37. package/css/components/suggest.css +10 -10
  38. package/css/components/tabs.css +22 -22
  39. package/css/components/theme-customizer.css +87 -87
  40. package/css/components/timeline.css +14 -14
  41. package/css/components/timepicker.css +7 -7
  42. package/css/components/toast.css +31 -31
  43. package/css/components/tooltips.css +11 -11
  44. package/css/components/transfer.css +12 -12
  45. package/css/components/tree.css +9 -9
  46. package/css/components/waypoint.css +3 -3
  47. package/css/core/colors.css +34 -34
  48. package/css/core/grid.css +1 -6
  49. package/css/core/helpers.css +11 -11
  50. package/css/core/tokens.css +113 -35
  51. package/css/core/typography.css +14 -12
  52. package/css/core/vd-aliases.css +100 -52
  53. package/css/effects/morph.css +5 -5
  54. package/css/utilities/media.css +2 -2
  55. package/css/utilities/table.css +34 -34
  56. package/css/utilities/transitions.css +22 -10
  57. package/css/vanduo.css +14 -34
  58. package/dist/build-info.json +3 -3
  59. package/dist/vanduo.cjs.js +929 -289
  60. package/dist/vanduo.cjs.js.map +3 -3
  61. package/dist/vanduo.cjs.min.js +7 -7
  62. package/dist/vanduo.cjs.min.js.map +3 -3
  63. package/dist/vanduo.css +7914 -7823
  64. package/dist/vanduo.css.map +1 -1
  65. package/dist/vanduo.esm.js +929 -289
  66. package/dist/vanduo.esm.js.map +3 -3
  67. package/dist/vanduo.esm.min.js +7 -7
  68. package/dist/vanduo.esm.min.js.map +3 -3
  69. package/dist/vanduo.js +929 -289
  70. package/dist/vanduo.js.map +3 -3
  71. package/dist/vanduo.min.css +2 -2
  72. package/dist/vanduo.min.css.map +1 -1
  73. package/dist/vanduo.min.js +7 -7
  74. package/dist/vanduo.min.js.map +3 -3
  75. package/js/components/affix.js +2 -2
  76. package/js/components/bubble.js +3 -3
  77. package/js/components/code-snippet.js +129 -5
  78. package/js/components/collapsible.js +2 -3
  79. package/js/components/datepicker.js +2 -2
  80. package/js/components/doc-search.js +69 -11
  81. package/js/components/draggable.js +4 -4
  82. package/js/components/dropdown.js +2 -3
  83. package/js/components/expanding-cards.js +2 -2
  84. package/js/components/flow.js +2 -2
  85. package/js/components/font-switcher.js +23 -13
  86. package/js/components/glass.js +2 -2
  87. package/js/components/grid.js +19 -8
  88. package/js/components/image-box.js +49 -10
  89. package/js/components/lazy-load.js +81 -9
  90. package/js/components/modals.js +28 -12
  91. package/js/components/morph.js +2 -2
  92. package/js/components/music-player.js +2 -2
  93. package/js/components/navbar.js +2 -2
  94. package/js/components/pagination.js +2 -3
  95. package/js/components/parallax.js +9 -10
  96. package/js/components/preloader.js +14 -5
  97. package/js/components/rating.js +2 -2
  98. package/js/components/ripple.js +2 -2
  99. package/js/components/select.js +2 -3
  100. package/js/components/sidenav.js +43 -14
  101. package/js/components/spotlight.js +2 -2
  102. package/js/components/stepper.js +2 -2
  103. package/js/components/suggest.js +9 -3
  104. package/js/components/tabs.js +2 -2
  105. package/js/components/theme-customizer.js +151 -21
  106. package/js/components/theme-switcher.js +27 -16
  107. package/js/components/timeline.js +41 -12
  108. package/js/components/timepicker.js +2 -2
  109. package/js/components/toast.js +1 -1
  110. package/js/components/tooltips.js +4 -4
  111. package/js/components/transfer.js +2 -2
  112. package/js/components/tree.js +2 -2
  113. package/js/components/validate.js +2 -2
  114. package/js/components/vd-hex.js +12 -6
  115. package/js/components/waypoint.js +2 -2
  116. package/js/utils/helpers.js +7 -4
  117. package/js/utils/lifecycle.js +158 -83
  118. package/js/vanduo.js +203 -34
  119. package/package.json +2 -1
@@ -36,8 +36,8 @@
36
36
  const Suggest = {
37
37
  instances: new Map(),
38
38
 
39
- init: function () {
40
- const inputs = document.querySelectorAll('[data-vd-suggest], [data-vd-autocomplete]');
39
+ init: function (root) {
40
+ const inputs = window.Vanduo.queryAll(root, '[data-vd-suggest], [data-vd-autocomplete]');
41
41
  inputs.forEach(el => {
42
42
  if (this.instances.has(el)) return;
43
43
  this.initInstance(el);
@@ -220,16 +220,22 @@
220
220
  const blurHandler = () => {
221
221
  setTimeout(close, 200);
222
222
  };
223
+ const focusHandler = () => {
224
+ if (input.value.length >= minChars) {
225
+ doSearch(input.value);
226
+ }
227
+ };
223
228
 
224
229
  input.addEventListener('input', inputHandler);
225
230
  input.addEventListener('keydown', keyHandler);
226
231
  input.addEventListener('blur', blurHandler);
227
- input.addEventListener('focus', () => { if (input.value.length >= minChars) doSearch(input.value); });
232
+ input.addEventListener('focus', focusHandler);
228
233
 
229
234
  cleanup.push(
230
235
  () => input.removeEventListener('input', inputHandler),
231
236
  () => input.removeEventListener('keydown', keyHandler),
232
237
  () => input.removeEventListener('blur', blurHandler),
238
+ () => input.removeEventListener('focus', focusHandler),
233
239
  () => clearTimeout(debounceTimer),
234
240
  () => { if (list.parentNode) list.parentNode.removeChild(list); }
235
241
  );
@@ -16,8 +16,8 @@
16
16
  /**
17
17
  * Initialize all tab components
18
18
  */
19
- init: function() {
20
- const tabContainers = document.querySelectorAll('.vd-tabs, [data-tabs]');
19
+ init: function(root) {
20
+ const tabContainers = window.Vanduo.queryAll(root, '.vd-tabs, [data-tabs]');
21
21
 
22
22
  tabContainers.forEach(container => {
23
23
  if (this.instances.has(container)) {
@@ -86,23 +86,77 @@
86
86
 
87
87
  isInitialized: false,
88
88
  _cleanup: [],
89
+ _ownsDynamicPanel: false,
89
90
 
90
91
  // DOM references
91
92
  elements: {
92
93
  customizer: null,
93
94
  trigger: null,
95
+ activeTrigger: null,
94
96
  triggers: [],
95
97
  panel: null,
96
98
  overlay: null
97
99
  },
98
100
 
101
+ isRoot: function (root) {
102
+ return !!root && (root === document || root.nodeType === 1 || root.nodeType === 9 || root.nodeType === 11);
103
+ },
104
+
105
+ normalizeRoot: function (root) {
106
+ return this.isRoot(root) ? root : document;
107
+ },
108
+
109
+ queryAll: function (root, selector) {
110
+ const scope = this.normalizeRoot(root);
111
+ if (typeof window.VanduoLifecycle !== 'undefined' && typeof window.VanduoLifecycle.queryAll === 'function') {
112
+ return window.VanduoLifecycle.queryAll(scope, selector);
113
+ }
114
+
115
+ const matches = [];
116
+ if (scope instanceof Element && typeof scope.matches === 'function' && scope.matches(selector)) {
117
+ matches.push(scope);
118
+ }
119
+ if (typeof scope.querySelectorAll === 'function') {
120
+ const descendants = scope.querySelectorAll(selector);
121
+ for (let i = 0; i < descendants.length; i++) {
122
+ matches.push(descendants[i]);
123
+ }
124
+ }
125
+ return matches;
126
+ },
127
+
128
+ queryOne: function (root, selector) {
129
+ const matches = this.queryAll(root, selector);
130
+ return matches.length ? matches[0] : null;
131
+ },
132
+
133
+ getTriggers: function (root) {
134
+ return this.queryAll(root, '[data-theme-customizer-trigger]');
135
+ },
136
+
137
+ pruneTriggers: function () {
138
+ this.elements.triggers = this.elements.triggers.filter(function (trigger) {
139
+ return trigger && trigger.isConnected;
140
+ });
141
+
142
+ if (this.elements.trigger && !this.elements.trigger.isConnected) {
143
+ this.elements.trigger = null;
144
+ }
145
+
146
+ if (this.elements.activeTrigger && !this.elements.activeTrigger.isConnected) {
147
+ this.elements.activeTrigger = null;
148
+ }
149
+ },
150
+
99
151
  /**
100
152
  * Initialize the Theme Customizer
101
153
  */
102
- init: function () {
154
+ init: function (root) {
155
+ const scope = this.normalizeRoot(root);
156
+
103
157
  if (this.isInitialized) {
104
- this.bindExistingElements();
105
- this.bindTriggerEvents();
158
+ this.bindExistingElements(scope);
159
+ this.bindTriggerEvents(scope);
106
160
  this.bindPanelEvents();
107
161
  this.updateUI();
108
162
  return;
@@ -113,10 +167,8 @@
113
167
 
114
168
  this.loadPreferences();
115
169
  this.applyAllPreferences();
116
- this.bindExistingElements();
117
- this.bindEvents();
118
-
119
- console.log('Vanduo Theme Customizer initialized');
170
+ this.bindExistingElements(scope);
171
+ this.bindEvents(scope);
120
172
  },
121
173
 
122
174
  addListener: function (target, event, handler, options) {
@@ -315,21 +367,33 @@
315
367
  /**
316
368
  * Bind to existing DOM elements or create them dynamically
317
369
  */
318
- bindExistingElements: function () {
319
- // First check for existing full structure
320
- this.elements.customizer = document.querySelector('.vd-theme-customizer');
321
- this.elements.triggers = Array.from(document.querySelectorAll('[data-theme-customizer-trigger]'));
370
+ bindExistingElements: function (root) {
371
+ const scope = this.normalizeRoot(root);
372
+ this.pruneTriggers();
373
+
374
+ const scopedTriggers = this.getTriggers(scope);
375
+ scopedTriggers.forEach((trigger) => {
376
+ if (!this.elements.triggers.includes(trigger)) {
377
+ this.elements.triggers.push(trigger);
378
+ }
379
+ });
380
+
322
381
  if (!this.elements.trigger && this.elements.triggers.length) {
323
382
  this.elements.trigger = this.elements.triggers[0];
324
383
  }
325
384
 
326
- if (this.elements.customizer) {
385
+ const existingCustomizer = this.queryOne(scope, '.vd-theme-customizer')
386
+ || (this.elements.customizer && typeof this.elements.customizer.contains === 'function' ? this.elements.customizer : null)
387
+ || document.querySelector('.vd-theme-customizer');
388
+
389
+ if (existingCustomizer instanceof Element) {
390
+ this.elements.customizer = existingCustomizer;
327
391
  this.elements.trigger = this.elements.customizer.querySelector('.vd-theme-customizer-trigger') || this.elements.trigger;
328
392
  this.elements.panel = this.elements.customizer.querySelector('.vd-theme-customizer-panel');
329
393
  this.elements.overlay = this.elements.customizer.querySelector('.vd-theme-customizer-overlay');
330
394
  } else {
331
395
  // Look for standalone trigger buttons with data attribute
332
- if (this.elements.triggers.length) {
396
+ if (scopedTriggers.length && !this.elements.panel) {
333
397
  this.createDynamicPanel();
334
398
  }
335
399
  }
@@ -342,7 +406,7 @@
342
406
  * Create the panel dynamically when only a trigger button exists
343
407
  */
344
408
  createDynamicPanel: function () {
345
- if (!this.elements.triggers.length) {
409
+ if (!this.elements.triggers.length || (this.elements.panel && this.elements.panel.isConnected)) {
346
410
  return;
347
411
  }
348
412
  this.elements.trigger = this.elements.triggers[0];
@@ -363,6 +427,7 @@
363
427
  // Store references
364
428
  this.elements.panel = panel;
365
429
  this.elements.overlay = overlay;
430
+ this._ownsDynamicPanel = true;
366
431
  this.elements.customizer = {
367
432
  contains: (el) => panel.contains(el) || this.elements.triggers.some((trigger) => trigger.contains(el))
368
433
  };
@@ -595,8 +660,8 @@
595
660
  }
596
661
  },
597
662
 
598
- bindEvents: function () {
599
- this.bindTriggerEvents();
663
+ bindEvents: function (root) {
664
+ this.bindTriggerEvents(root);
600
665
 
601
666
  this.bindPanelEvents();
602
667
 
@@ -631,22 +696,53 @@
631
696
  });
632
697
  },
633
698
 
634
- bindTriggerEvents: function () {
635
- this.elements.triggers.forEach((trigger) => {
699
+ bindTriggerEvents: function (root) {
700
+ const triggers = root ? this.getTriggers(root) : this.elements.triggers;
701
+ triggers.forEach((trigger) => {
636
702
  if (trigger.getAttribute('data-customizer-trigger-initialized') === 'true') {
637
703
  return;
638
704
  }
639
- this.addListener(trigger, 'click', (e) => {
705
+
706
+ const onClick = (e) => {
640
707
  e.preventDefault();
641
708
  e.stopPropagation();
642
709
  this.elements.activeTrigger = trigger;
643
710
  this.elements.trigger = trigger;
644
711
  this.toggle();
645
- });
712
+ };
713
+
714
+ trigger.addEventListener('click', onClick);
715
+ trigger._themeCustomizerTriggerHandler = onClick;
646
716
  trigger.setAttribute('data-customizer-trigger-initialized', 'true');
647
717
  });
648
718
  },
649
719
 
720
+ cleanupTrigger: function (trigger) {
721
+ if (!trigger || trigger.getAttribute('data-customizer-trigger-initialized') !== 'true') {
722
+ return;
723
+ }
724
+
725
+ if (trigger._themeCustomizerTriggerHandler) {
726
+ trigger.removeEventListener('click', trigger._themeCustomizerTriggerHandler);
727
+ delete trigger._themeCustomizerTriggerHandler;
728
+ }
729
+
730
+ trigger.setAttribute('aria-expanded', 'false');
731
+ trigger.removeAttribute('data-customizer-trigger-initialized');
732
+
733
+ if (this.elements.activeTrigger === trigger) {
734
+ this.elements.activeTrigger = null;
735
+ }
736
+
737
+ this.elements.triggers = this.elements.triggers.filter(function (candidate) {
738
+ return candidate !== trigger;
739
+ });
740
+
741
+ if (this.elements.trigger === trigger) {
742
+ this.elements.trigger = this.elements.triggers[0] || null;
743
+ }
744
+ },
745
+
650
746
  /**
651
747
  * Toggle panel open/close
652
748
  */
@@ -782,7 +878,21 @@
782
878
  }
783
879
  },
784
880
 
785
- destroyAll: function () {
881
+ destroyAll: function (root) {
882
+ const scope = this.normalizeRoot(root);
883
+
884
+ if (scope !== document) {
885
+ this.getTriggers(scope).forEach((trigger) => {
886
+ this.cleanupTrigger(trigger);
887
+ });
888
+ this.pruneTriggers();
889
+
890
+ if (!this.elements.triggers.length) {
891
+ this.destroyAll(document);
892
+ }
893
+ return;
894
+ }
895
+
786
896
  this._cleanup.forEach(fn => fn());
787
897
  this._cleanup = [];
788
898
 
@@ -790,7 +900,27 @@
790
900
  this.elements.panel.removeAttribute('data-customizer-initialized');
791
901
  }
792
902
 
903
+ this.elements.triggers.slice().forEach((trigger) => {
904
+ this.cleanupTrigger(trigger);
905
+ });
906
+
907
+ if (this._ownsDynamicPanel) {
908
+ if (this.elements.panel && this.elements.panel.parentNode) {
909
+ this.elements.panel.parentNode.removeChild(this.elements.panel);
910
+ }
911
+ if (this.elements.overlay && this.elements.overlay.parentNode) {
912
+ this.elements.overlay.parentNode.removeChild(this.elements.overlay);
913
+ }
914
+ this._ownsDynamicPanel = false;
915
+ }
916
+
793
917
  this.close();
918
+ this.elements.customizer = null;
919
+ this.elements.trigger = null;
920
+ this.elements.activeTrigger = null;
921
+ this.elements.triggers = [];
922
+ this.elements.panel = null;
923
+ this.elements.overlay = null;
794
924
  this.isInitialized = false;
795
925
  }
796
926
  };
@@ -11,7 +11,15 @@
11
11
  _mediaQuery: null,
12
12
  _onMediaChange: null,
13
13
 
14
- init: function () {
14
+ getToggles: function (root) {
15
+ if (window.Vanduo && typeof window.Vanduo.queryAll === 'function') {
16
+ return window.Vanduo.queryAll(root, '[data-toggle="theme"]');
17
+ }
18
+
19
+ return Array.from(document.querySelectorAll('[data-toggle="theme"]'));
20
+ },
21
+
22
+ init: function (root) {
15
23
  this.STORAGE_KEY = 'vanduo-theme-preference';
16
24
  this.state = {
17
25
  preference: this.getPreference() // 'light', 'dark', or 'system'
@@ -19,8 +27,8 @@
19
27
 
20
28
  if (this.isInitialized) {
21
29
  this.applyTheme();
22
- this.renderUI();
23
- this.updateUI();
30
+ this.renderUI(root);
31
+ this.updateUI(root);
24
32
  return;
25
33
  }
26
34
 
@@ -28,9 +36,7 @@
28
36
 
29
37
  this.applyTheme();
30
38
  this.listenForSystemChanges();
31
- this.renderUI();
32
-
33
- console.log('Vanduo Theme Switcher initialized');
39
+ this.renderUI(root);
34
40
  },
35
41
 
36
42
  getPreference: function () {
@@ -113,9 +119,9 @@
113
119
  },
114
120
 
115
121
  // Helper to facilitate UI creation if needed, though often UI is in HTML
116
- renderUI: function () {
122
+ renderUI: function (root) {
117
123
  // Look for any uninitialized theme toggles
118
- const toggles = document.querySelectorAll('[data-toggle="theme"]');
124
+ const toggles = this.getToggles(root);
119
125
  toggles.forEach(toggle => {
120
126
  if (toggle.getAttribute('data-theme-initialized') === 'true') {
121
127
  if (toggle.tagName === 'SELECT') {
@@ -147,8 +153,8 @@
147
153
  });
148
154
  },
149
155
 
150
- updateUI: function () {
151
- const toggles = document.querySelectorAll('[data-toggle="theme"]');
156
+ updateUI: function (root) {
157
+ const toggles = this.getToggles(root);
152
158
  toggles.forEach(toggle => {
153
159
  if (toggle.tagName === 'SELECT') {
154
160
  toggle.value = this.state.preference;
@@ -166,8 +172,11 @@
166
172
  });
167
173
  },
168
174
 
169
- destroyAll: function () {
170
- const toggles = document.querySelectorAll('[data-toggle="theme"][data-theme-initialized="true"]');
175
+ destroyAll: function (root) {
176
+ const scope = root || document;
177
+ const toggles = this.getToggles(scope).filter(function (toggle) {
178
+ return toggle.getAttribute('data-theme-initialized') === 'true';
179
+ });
171
180
  toggles.forEach(toggle => {
172
181
  if (toggle._themeToggleHandler) {
173
182
  const eventName = toggle.tagName === 'SELECT' ? 'change' : 'click';
@@ -177,13 +186,15 @@
177
186
  toggle.removeAttribute('data-theme-initialized');
178
187
  });
179
188
 
180
- if (this._mediaQuery && this._onMediaChange) {
189
+ if (scope === document && this._mediaQuery && this._onMediaChange) {
181
190
  this._mediaQuery.removeEventListener('change', this._onMediaChange);
182
191
  }
183
192
 
184
- this._mediaQuery = null;
185
- this._onMediaChange = null;
186
- this.isInitialized = false;
193
+ if (scope === document) {
194
+ this._mediaQuery = null;
195
+ this._onMediaChange = null;
196
+ this.isInitialized = false;
197
+ }
187
198
  }
188
199
  };
189
200
 
@@ -37,6 +37,8 @@
37
37
  const pauseBtn = scope.querySelector('[data-vd-timeline-pause]');
38
38
 
39
39
  let playTimer = null;
40
+ let isPlaying = false;
41
+ let playToken = 0;
40
42
 
41
43
  function updateNavButtons() {
42
44
  const k = countRevealedPrefix(items);
@@ -52,10 +54,10 @@
52
54
  nextBtn.setAttribute('aria-disabled', atEnd ? 'true' : 'false');
53
55
  }
54
56
  if (playBtn) {
55
- playBtn.setAttribute('aria-pressed', playTimer ? 'true' : 'false');
57
+ playBtn.setAttribute('aria-pressed', isPlaying ? 'true' : 'false');
56
58
  }
57
59
  if (pauseBtn) {
58
- pauseBtn.disabled = !playTimer;
60
+ pauseBtn.disabled = !isPlaying;
59
61
  }
60
62
  }
61
63
 
@@ -75,21 +77,43 @@
75
77
  updateNavButtons();
76
78
  }
77
79
 
78
- function play() {
79
- if (playTimer) return;
80
- playTimer = setInterval(function () {
80
+ function scheduleNext() {
81
+ const token = ++playToken;
82
+ playTimer = setTimeout(function () {
83
+ playTimer = null;
84
+
85
+ if (!isPlaying || token !== playToken) {
86
+ return;
87
+ }
88
+
81
89
  if (countRevealedPrefix(items) >= items.length) {
82
90
  pause();
83
91
  return;
84
92
  }
93
+
85
94
  stepNext();
95
+
96
+ if (countRevealedPrefix(items) >= items.length) {
97
+ pause();
98
+ return;
99
+ }
100
+
101
+ scheduleNext();
86
102
  }, PLAY_INTERVAL_MS);
103
+ }
104
+
105
+ function play() {
106
+ if (isPlaying) return;
107
+ isPlaying = true;
108
+ scheduleNext();
87
109
  updateNavButtons();
88
110
  }
89
111
 
90
112
  function pause() {
113
+ isPlaying = false;
114
+ playToken++;
91
115
  if (playTimer) {
92
- clearInterval(playTimer);
116
+ clearTimeout(playTimer);
93
117
  playTimer = null;
94
118
  }
95
119
  updateNavButtons();
@@ -129,16 +153,16 @@
129
153
  const Timeline = {
130
154
  instances: new Map(),
131
155
 
132
- init: function () {
133
- document.querySelectorAll('.vd-timeline.vd-timeline-animated').forEach(function (el) {
156
+ init: function (root) {
157
+ window.Vanduo.queryAll(root, '.vd-timeline.vd-timeline-animated').forEach(function (el) {
134
158
  if (Timeline.instances.has(el)) return;
135
159
  Timeline.initInstance(el);
136
160
  });
137
161
  },
138
162
 
139
- reinit: function () {
140
- Timeline.destroyAll();
141
- Timeline.init();
163
+ reinit: function (root) {
164
+ Timeline.destroyAll(root);
165
+ Timeline.init(root);
142
166
  },
143
167
 
144
168
  initInstance: function (container) {
@@ -211,8 +235,13 @@
211
235
  this.instances.delete(container);
212
236
  },
213
237
 
214
- destroyAll: function () {
238
+ destroyAll: function (root) {
239
+ const scope = window.Vanduo && typeof window.Vanduo._normalizeRoot === 'function'
240
+ ? window.Vanduo._normalizeRoot(root)
241
+ : document;
242
+
215
243
  this.instances.forEach(function (_, el) {
244
+ if (scope !== document && scope !== el && (!scope.contains || !scope.contains(el))) return;
216
245
  Timeline.destroy(el);
217
246
  });
218
247
  }
@@ -9,8 +9,8 @@
9
9
  const Timepicker = {
10
10
  instances: new Map(),
11
11
 
12
- init: function () {
13
- const inputs = document.querySelectorAll('[data-vd-timepicker]');
12
+ init: function (root) {
13
+ const inputs = window.Vanduo.queryAll(root, '[data-vd-timepicker]');
14
14
  inputs.forEach(el => {
15
15
  if (this.instances.has(el)) return;
16
16
  this.initInstance(el);
@@ -94,7 +94,7 @@
94
94
  if (config.icon) {
95
95
  const allowSvg = config.iconAllowSvg === true;
96
96
  const safeIcon = typeof sanitizeHtml === 'function'
97
- ? sanitizeHtml(config.icon, { allowSvg })
97
+ ? sanitizeHtml(config.icon, { allowSvg, allowStyle: false })
98
98
  : escapeHtml(config.icon);
99
99
  html += `<span class="vd-toast-icon">${safeIcon}</span>`;
100
100
  } else if (config.type) {
@@ -31,8 +31,8 @@
31
31
  /**
32
32
  * Initialize tooltips
33
33
  */
34
- init: function () {
35
- const elements = document.querySelectorAll('[data-tooltip], [data-tooltip-html]');
34
+ init: function (root) {
35
+ const elements = window.Vanduo.queryAll(root, '[data-tooltip], [data-tooltip-html]');
36
36
 
37
37
  elements.forEach(element => {
38
38
  if (this.tooltips.has(element)) {
@@ -93,7 +93,7 @@
93
93
 
94
94
  if (htmlContent) {
95
95
  const allowSvg = element.hasAttribute('data-tooltip-allow-svg');
96
- tooltip.innerHTML = this.sanitizeHtml(htmlContent, { allowSvg });
96
+ tooltip.innerHTML = this.sanitizeHtml(htmlContent, { allowSvg, allowStyle: false });
97
97
  tooltip.classList.add('vd-tooltip-html');
98
98
  } else if (textContent) {
99
99
  tooltip.textContent = textContent;
@@ -253,7 +253,7 @@
253
253
  const { tooltip } = this.tooltips.get(el);
254
254
  if (isHtml) {
255
255
  const allowSvg = el.hasAttribute('data-tooltip-allow-svg');
256
- tooltip.innerHTML = this.sanitizeHtml(content, { allowSvg });
256
+ tooltip.innerHTML = this.sanitizeHtml(content, { allowSvg, allowStyle: false });
257
257
  tooltip.classList.add('vd-tooltip-html');
258
258
  } else {
259
259
  tooltip.textContent = content;
@@ -9,8 +9,8 @@
9
9
  const Transfer = {
10
10
  instances: new Map(),
11
11
 
12
- init: function () {
13
- const transfers = document.querySelectorAll('[data-vd-transfer]');
12
+ init: function (root) {
13
+ const transfers = window.Vanduo.queryAll(root, '[data-vd-transfer]');
14
14
  transfers.forEach(el => {
15
15
  if (this.instances.has(el)) return;
16
16
  this.initInstance(el);
@@ -9,8 +9,8 @@
9
9
  const Tree = {
10
10
  instances: new Map(),
11
11
 
12
- init: function () {
13
- const trees = document.querySelectorAll('[data-vd-tree]');
12
+ init: function (root) {
13
+ const trees = window.Vanduo.queryAll(root, '[data-vd-tree]');
14
14
  trees.forEach(el => {
15
15
  if (this.instances.has(el)) return;
16
16
  this.initInstance(el);
@@ -49,8 +49,8 @@
49
49
  match: 'Fields do not match'
50
50
  },
51
51
 
52
- init: function () {
53
- const forms = document.querySelectorAll('[data-vd-validate], .vd-validate');
52
+ init: function (root) {
53
+ const forms = window.Vanduo.queryAll(root, '[data-vd-validate], .vd-validate');
54
54
  forms.forEach(form => {
55
55
  if (this.instances.has(form)) return;
56
56
  this.initInstance(form);
@@ -90,13 +90,19 @@ export class VdHexGrid {
90
90
  const root = document.documentElement;
91
91
  const style = getComputedStyle(root);
92
92
 
93
+ const readToken = (canonical, fallback, defaultValue) => {
94
+ return style.getPropertyValue(canonical).trim()
95
+ || style.getPropertyValue(fallback).trim()
96
+ || defaultValue;
97
+ };
98
+
93
99
  return {
94
- bgPrimary: style.getPropertyValue('--bg-primary').trim() || '#ffffff',
95
- bgSecondary: style.getPropertyValue('--bg-secondary').trim() || '#f5f5f5',
96
- borderColor: style.getPropertyValue('--border-color').trim() || '#e0e0e0',
97
- colorPrimary: style.getPropertyValue('--color-primary').trim() || '#3b82f6',
98
- textColor: style.getPropertyValue('--text-primary').trim() || '#1f2937',
99
- textMuted: style.getPropertyValue('--text-muted').trim() || '#6b7280'
100
+ bgPrimary: readToken('--vd-bg-primary', '--bg-primary', '#ffffff'),
101
+ bgSecondary: readToken('--vd-bg-secondary', '--bg-secondary', '#f5f5f5'),
102
+ borderColor: readToken('--vd-border-color', '--border-color', '#e0e0e0'),
103
+ colorPrimary: readToken('--vd-color-primary', '--color-primary', '#3b82f6'),
104
+ textColor: readToken('--vd-text-primary', '--text-primary', '#1f2937'),
105
+ textMuted: readToken('--vd-text-muted', '--text-muted', '#6b7280')
100
106
  };
101
107
  }
102
108
 
@@ -9,8 +9,8 @@
9
9
  const Waypoint = {
10
10
  instances: new Map(),
11
11
 
12
- init: function () {
13
- const navs = document.querySelectorAll('[data-vd-waypoint-nav], [data-vd-scrollspy-nav]');
12
+ init: function (root) {
13
+ const navs = window.Vanduo.queryAll(root, '[data-vd-waypoint-nav], [data-vd-scrollspy-nav]');
14
14
  navs.forEach(nav => {
15
15
  if (this.instances.has(nav)) return;
16
16
  this.initInstance(nav);
@@ -258,7 +258,7 @@ function escapeHtml(str) {
258
258
  * Keeps a small set of tags and strips disallowed tags and attributes. Safe for
259
259
  * simple rich text (use server-side or DOMPurify for stronger guarantees).
260
260
  * @param {string} input
261
- * @param {{ allowSvg?: boolean }} [options]
261
+ * @param {{ allowSvg?: boolean, allowStyle?: boolean }} [options]
262
262
  * @returns {string} sanitized HTML
263
263
  */
264
264
  function sanitizeHtml(input, options = {}) {
@@ -271,6 +271,7 @@ function sanitizeHtml(input, options = {}) {
271
271
  return escapeHtml(input);
272
272
  }
273
273
  const allowSvg = options && options.allowSvg === true;
274
+ const allowStyle = !options || options.allowStyle !== false;
274
275
  const baseAllowed = ['B', 'STRONG', 'I', 'EM', 'BR', 'A', 'SPAN', 'U', 'DIV', 'P', 'KBD', 'CODE', 'SMALL', 'MARK'];
275
276
  const svgAllowed = ['SVG', 'PATH', 'LINE', 'CIRCLE', 'POLYLINE', 'RECT', 'G'];
276
277
  const allowed = allowSvg ? baseAllowed.concat(svgAllowed) : baseAllowed;
@@ -310,8 +311,11 @@ function sanitizeHtml(input, options = {}) {
310
311
  }
311
312
  });
312
313
  } else {
313
- // Keep class and style; strip everything else (no event handlers, no src, etc.)
314
- const safeAttrs = new Set(['class', 'style']);
314
+ // Keep class and optionally inline style; strip everything else.
315
+ const safeAttrs = new Set(['class']);
316
+ if (allowStyle) {
317
+ safeAttrs.add('style');
318
+ }
315
319
  const otherAttrs = Array.from(child.attributes || []);
316
320
  otherAttrs.forEach(function (a) {
317
321
  if (!safeAttrs.has(a.name)) { child.removeAttribute(a.name); }
@@ -327,4 +331,3 @@ function sanitizeHtml(input, options = {}) {
327
331
  }
328
332
 
329
333
 
330
-