vanduo-framework 1.1.8-docs-update

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 (196) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +216 -0
  3. package/css/components/alerts.css +224 -0
  4. package/css/components/avatar.css +275 -0
  5. package/css/components/badges.css +230 -0
  6. package/css/components/breadcrumbs.css +146 -0
  7. package/css/components/button-group.css +82 -0
  8. package/css/components/buttons.css +530 -0
  9. package/css/components/cards.css +304 -0
  10. package/css/components/chips.css +259 -0
  11. package/css/components/code-snippet.css +555 -0
  12. package/css/components/collapsible.css +267 -0
  13. package/css/components/collections.css +253 -0
  14. package/css/components/doc-search.css +464 -0
  15. package/css/components/doc-tabs.css +38 -0
  16. package/css/components/draggable.css +317 -0
  17. package/css/components/dropdown.css +266 -0
  18. package/css/components/footer.css +375 -0
  19. package/css/components/forms.css +1774 -0
  20. package/css/components/image-box.css +279 -0
  21. package/css/components/modals.css +285 -0
  22. package/css/components/navbar.css +530 -0
  23. package/css/components/pagination.css +186 -0
  24. package/css/components/preloader.css +340 -0
  25. package/css/components/progress.css +107 -0
  26. package/css/components/sidenav.css +301 -0
  27. package/css/components/skeleton.css +241 -0
  28. package/css/components/spinner.css +144 -0
  29. package/css/components/tabs.css +327 -0
  30. package/css/components/theme-customizer.css +835 -0
  31. package/css/components/toast.css +357 -0
  32. package/css/components/tooltips.css +270 -0
  33. package/css/core/colors.css +1017 -0
  34. package/css/core/fonts.css +266 -0
  35. package/css/core/grid.css +1699 -0
  36. package/css/core/helpers.css +2202 -0
  37. package/css/core/reset.css +128 -0
  38. package/css/core/tokens.css +213 -0
  39. package/css/core/typography.css +405 -0
  40. package/css/core/vd-aliases.css +47 -0
  41. package/css/effects/parallax.css +113 -0
  42. package/css/icons/icons-all.css +23 -0
  43. package/css/icons/icons.css +25 -0
  44. package/css/utilities/media.css +167 -0
  45. package/css/utilities/print.css +111 -0
  46. package/css/utilities/shadow.css +243 -0
  47. package/css/utilities/table.css +381 -0
  48. package/css/utilities/transforms.css +71 -0
  49. package/css/utilities/transitions.css +87 -0
  50. package/css/vanduo.css +80 -0
  51. package/dist/build-info.json +6 -0
  52. package/dist/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
  53. package/dist/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
  54. package/dist/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
  55. package/dist/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
  56. package/dist/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
  57. package/dist/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
  58. package/dist/fonts/inter/inter-bold.woff2 +0 -0
  59. package/dist/fonts/inter/inter-medium.woff2 +0 -0
  60. package/dist/fonts/inter/inter-regular.woff2 +0 -0
  61. package/dist/fonts/inter/inter-semibold.woff2 +0 -0
  62. package/dist/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
  63. package/dist/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
  64. package/dist/fonts/open-sans/open-sans-bold.woff2 +0 -0
  65. package/dist/fonts/open-sans/open-sans-medium.woff2 +0 -0
  66. package/dist/fonts/open-sans/open-sans-regular.woff2 +0 -0
  67. package/dist/fonts/rubik/rubik-bold.woff2 +0 -0
  68. package/dist/fonts/rubik/rubik-medium.woff2 +0 -0
  69. package/dist/fonts/rubik/rubik-regular.woff2 +0 -0
  70. package/dist/fonts/source-sans/source-sans-bold.woff2 +0 -0
  71. package/dist/fonts/source-sans/source-sans-regular.woff2 +0 -0
  72. package/dist/fonts/source-sans/source-sans-semibold.woff2 +0 -0
  73. package/dist/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
  74. package/dist/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
  75. package/dist/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
  76. package/dist/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
  77. package/dist/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
  78. package/dist/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
  79. package/dist/icons/phosphor/LICENSE +21 -0
  80. package/dist/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
  81. package/dist/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
  82. package/dist/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
  83. package/dist/icons/phosphor/bold/style.css +4627 -0
  84. package/dist/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
  85. package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
  86. package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
  87. package/dist/icons/phosphor/duotone/style.css +12115 -0
  88. package/dist/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
  89. package/dist/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
  90. package/dist/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
  91. package/dist/icons/phosphor/fill/style.css +4627 -0
  92. package/dist/icons/phosphor/light/Phosphor-Light.ttf +0 -0
  93. package/dist/icons/phosphor/light/Phosphor-Light.woff +0 -0
  94. package/dist/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
  95. package/dist/icons/phosphor/light/style.css +4627 -0
  96. package/dist/icons/phosphor/regular/Phosphor.ttf +0 -0
  97. package/dist/icons/phosphor/regular/Phosphor.woff +0 -0
  98. package/dist/icons/phosphor/regular/Phosphor.woff2 +0 -0
  99. package/dist/icons/phosphor/regular/style.css +4627 -0
  100. package/dist/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
  101. package/dist/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
  102. package/dist/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
  103. package/dist/icons/phosphor/thin/style.css +4627 -0
  104. package/dist/vanduo.cjs.js +5569 -0
  105. package/dist/vanduo.cjs.js.map +7 -0
  106. package/dist/vanduo.cjs.min.js +48 -0
  107. package/dist/vanduo.cjs.min.js.map +7 -0
  108. package/dist/vanduo.css +60666 -0
  109. package/dist/vanduo.css.map +1 -0
  110. package/dist/vanduo.esm.js +5548 -0
  111. package/dist/vanduo.esm.js.map +7 -0
  112. package/dist/vanduo.esm.min.js +48 -0
  113. package/dist/vanduo.esm.min.js.map +7 -0
  114. package/dist/vanduo.js +5545 -0
  115. package/dist/vanduo.js.map +7 -0
  116. package/dist/vanduo.min.css +2 -0
  117. package/dist/vanduo.min.css.map +1 -0
  118. package/dist/vanduo.min.js +48 -0
  119. package/dist/vanduo.min.js.map +7 -0
  120. package/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
  121. package/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
  122. package/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
  123. package/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
  124. package/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
  125. package/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
  126. package/fonts/inter/inter-bold.woff2 +0 -0
  127. package/fonts/inter/inter-medium.woff2 +0 -0
  128. package/fonts/inter/inter-regular.woff2 +0 -0
  129. package/fonts/inter/inter-semibold.woff2 +0 -0
  130. package/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
  131. package/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
  132. package/fonts/open-sans/open-sans-bold.woff2 +0 -0
  133. package/fonts/open-sans/open-sans-medium.woff2 +0 -0
  134. package/fonts/open-sans/open-sans-regular.woff2 +0 -0
  135. package/fonts/rubik/rubik-bold.woff2 +0 -0
  136. package/fonts/rubik/rubik-medium.woff2 +0 -0
  137. package/fonts/rubik/rubik-regular.woff2 +0 -0
  138. package/fonts/source-sans/source-sans-bold.woff2 +0 -0
  139. package/fonts/source-sans/source-sans-regular.woff2 +0 -0
  140. package/fonts/source-sans/source-sans-semibold.woff2 +0 -0
  141. package/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
  142. package/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
  143. package/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
  144. package/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
  145. package/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
  146. package/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
  147. package/icons/phosphor/LICENSE +21 -0
  148. package/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
  149. package/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
  150. package/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
  151. package/icons/phosphor/bold/style.css +4627 -0
  152. package/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
  153. package/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
  154. package/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
  155. package/icons/phosphor/duotone/style.css +12115 -0
  156. package/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
  157. package/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
  158. package/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
  159. package/icons/phosphor/fill/style.css +4627 -0
  160. package/icons/phosphor/light/Phosphor-Light.ttf +0 -0
  161. package/icons/phosphor/light/Phosphor-Light.woff +0 -0
  162. package/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
  163. package/icons/phosphor/light/style.css +4627 -0
  164. package/icons/phosphor/regular/Phosphor.ttf +0 -0
  165. package/icons/phosphor/regular/Phosphor.woff +0 -0
  166. package/icons/phosphor/regular/Phosphor.woff2 +0 -0
  167. package/icons/phosphor/regular/style.css +4627 -0
  168. package/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
  169. package/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
  170. package/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
  171. package/icons/phosphor/thin/style.css +4627 -0
  172. package/js/components/code-snippet.js +639 -0
  173. package/js/components/collapsible.js +226 -0
  174. package/js/components/doc-search.js +936 -0
  175. package/js/components/draggable.js +725 -0
  176. package/js/components/dropdown.js +362 -0
  177. package/js/components/font-switcher.js +253 -0
  178. package/js/components/grid.js +279 -0
  179. package/js/components/image-box.js +372 -0
  180. package/js/components/modals.js +367 -0
  181. package/js/components/navbar.js +264 -0
  182. package/js/components/pagination.js +286 -0
  183. package/js/components/parallax.js +216 -0
  184. package/js/components/preloader.js +183 -0
  185. package/js/components/select.js +444 -0
  186. package/js/components/sidenav.js +303 -0
  187. package/js/components/tabs.js +303 -0
  188. package/js/components/theme-customizer.js +784 -0
  189. package/js/components/theme-switcher.js +183 -0
  190. package/js/components/toast.js +343 -0
  191. package/js/components/tooltips.js +306 -0
  192. package/js/index.js +52 -0
  193. package/js/utils/helpers.js +306 -0
  194. package/js/utils/lifecycle.js +135 -0
  195. package/js/vanduo.js +120 -0
  196. package/package.json +78 -0
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Vanduo Framework - Sidenav Component
3
+ * JavaScript functionality for side navigation drawer
4
+ */
5
+
6
+ (function() {
7
+ 'use strict';
8
+
9
+ /**
10
+ * Sidenav Component
11
+ */
12
+ const Sidenav = {
13
+ sidenavs: new Map(),
14
+ breakpoint: 992, // Desktop breakpoint
15
+
16
+ // Global cleanup functions (toggles, resize)
17
+ _globalCleanups: [],
18
+
19
+ isFixedVariant: function(sidenav) {
20
+ return sidenav.classList.contains('vd-sidenav-fixed') || sidenav.classList.contains('sidenav-fixed');
21
+ },
22
+
23
+ isPushVariant: function(sidenav) {
24
+ return sidenav.classList.contains('vd-sidenav-push') || sidenav.classList.contains('sidenav-push');
25
+ },
26
+
27
+ isRightVariant: function(sidenav) {
28
+ return sidenav.classList.contains('vd-sidenav-right') || sidenav.classList.contains('sidenav-right');
29
+ },
30
+
31
+ /**
32
+ * Initialize sidenav components
33
+ */
34
+ init: function() {
35
+ const sidenavs = document.querySelectorAll('.vd-sidenav');
36
+
37
+ sidenavs.forEach(sidenav => {
38
+ if (this.sidenavs.has(sidenav)) {
39
+ return;
40
+ }
41
+ this.initSidenav(sidenav);
42
+ });
43
+
44
+ // Handle toggle buttons
45
+ const toggles = document.querySelectorAll('[data-sidenav-toggle]');
46
+ toggles.forEach(toggle => {
47
+ if (toggle.dataset.sidenavToggleInitialized) return;
48
+ toggle.dataset.sidenavToggleInitialized = 'true';
49
+
50
+ const toggleClickHandler = (e) => {
51
+ e.preventDefault();
52
+ const targetId = toggle.dataset.sidenavToggle;
53
+ const sidenav = document.querySelector(targetId);
54
+ if (sidenav) {
55
+ this.toggle(sidenav);
56
+ }
57
+ };
58
+ toggle.addEventListener('click', toggleClickHandler);
59
+ this._globalCleanups.push(() => toggle.removeEventListener('click', toggleClickHandler));
60
+ });
61
+
62
+ // Handle responsive behavior
63
+ this.handleResize();
64
+ const resizeHandler = () => {
65
+ this.handleResize();
66
+ };
67
+ window.addEventListener('resize', resizeHandler);
68
+ this._globalCleanups.push(() => window.removeEventListener('resize', resizeHandler));
69
+ },
70
+
71
+ /**
72
+ * Initialize a single sidenav
73
+ * @param {HTMLElement} sidenav - Sidenav element
74
+ */
75
+ initSidenav: function(sidenav) {
76
+ const overlay = this.createOverlay(sidenav);
77
+ const closeButton = sidenav.querySelector('.vd-sidenav-close');
78
+ const cleanupFunctions = [];
79
+
80
+ // Set ARIA attributes
81
+ sidenav.setAttribute('role', 'navigation');
82
+ sidenav.setAttribute('aria-hidden', 'true');
83
+
84
+ // Close button handler
85
+ if (closeButton) {
86
+ const closeHandler = () => {
87
+ this.close(sidenav);
88
+ };
89
+ closeButton.addEventListener('click', closeHandler);
90
+ cleanupFunctions.push(() => closeButton.removeEventListener('click', closeHandler));
91
+ }
92
+
93
+ // Overlay click handler
94
+ const overlayClickHandler = () => {
95
+ if (sidenav.dataset.backdrop !== 'static') {
96
+ this.close(sidenav);
97
+ }
98
+ };
99
+ overlay.addEventListener('click', overlayClickHandler);
100
+ cleanupFunctions.push(() => overlay.removeEventListener('click', overlayClickHandler));
101
+
102
+ // ESC key handler
103
+ const escKeyHandler = (e) => {
104
+ if (e.key === 'Escape' && sidenav.classList.contains('is-open')) {
105
+ if (sidenav.dataset.keyboard !== 'false') {
106
+ this.close(sidenav);
107
+ }
108
+ }
109
+ };
110
+ document.addEventListener('keydown', escKeyHandler);
111
+ cleanupFunctions.push(() => document.removeEventListener('keydown', escKeyHandler));
112
+
113
+ this.sidenavs.set(sidenav, { overlay, cleanup: cleanupFunctions });
114
+ },
115
+
116
+ /**
117
+ * Create overlay element
118
+ * @param {HTMLElement} sidenav - Sidenav element
119
+ * @returns {HTMLElement} Overlay element
120
+ */
121
+ createOverlay: function(sidenav) {
122
+ let overlay = sidenav.querySelector('.vd-sidenav-overlay');
123
+
124
+ if (!overlay) {
125
+ overlay = document.createElement('div');
126
+ overlay.className = 'vd-sidenav-overlay';
127
+ document.body.appendChild(overlay);
128
+ }
129
+
130
+ return overlay;
131
+ },
132
+
133
+ /**
134
+ * Open sidenav
135
+ * @param {HTMLElement|string} sidenav - Sidenav element or selector
136
+ */
137
+ open: function(sidenav) {
138
+ const el = typeof sidenav === 'string' ? document.querySelector(sidenav) : sidenav;
139
+
140
+ if (!el || !this.sidenavs.has(el)) {
141
+ return;
142
+ }
143
+
144
+ const { overlay } = this.sidenavs.get(el);
145
+
146
+ // Show overlay (if not fixed)
147
+ if (!this.isFixedVariant(el)) {
148
+ overlay.classList.add('is-visible');
149
+ }
150
+
151
+ // Open sidenav
152
+ el.classList.add('is-open');
153
+ el.setAttribute('aria-hidden', 'false');
154
+
155
+ // Lock body scroll
156
+ document.body.classList.add('body-sidenav-open');
157
+
158
+ // Handle push variant
159
+ if (this.isPushVariant(el)) {
160
+ this.handlePushVariant(el, true);
161
+ }
162
+
163
+ // Dispatch event
164
+ el.dispatchEvent(new CustomEvent('sidenav:open', { bubbles: true }));
165
+ },
166
+
167
+ /**
168
+ * Close sidenav
169
+ * @param {HTMLElement|string} sidenav - Sidenav element or selector
170
+ */
171
+ close: function(sidenav) {
172
+ const el = typeof sidenav === 'string' ? document.querySelector(sidenav) : sidenav;
173
+
174
+ if (!el || !this.sidenavs.has(el)) {
175
+ return;
176
+ }
177
+
178
+ const { overlay } = this.sidenavs.get(el);
179
+
180
+ // Hide overlay
181
+ overlay.classList.remove('is-visible');
182
+
183
+ // Close sidenav
184
+ el.classList.remove('is-open');
185
+ el.setAttribute('aria-hidden', 'true');
186
+
187
+ // Unlock body scroll
188
+ document.body.classList.remove('body-sidenav-open');
189
+
190
+ // Handle push variant
191
+ if (this.isPushVariant(el)) {
192
+ this.handlePushVariant(el, false);
193
+ }
194
+
195
+ // Dispatch event
196
+ el.dispatchEvent(new CustomEvent('sidenav:close', { bubbles: true }));
197
+ },
198
+
199
+ /**
200
+ * Toggle sidenav
201
+ * @param {HTMLElement|string} sidenav - Sidenav element or selector
202
+ */
203
+ toggle: function(sidenav) {
204
+ const el = typeof sidenav === 'string' ? document.querySelector(sidenav) : sidenav;
205
+ if (el) {
206
+ if (el.classList.contains('is-open')) {
207
+ this.close(el);
208
+ } else {
209
+ this.open(el);
210
+ }
211
+ }
212
+ },
213
+
214
+ /**
215
+ * Handle push variant
216
+ * @param {HTMLElement} sidenav - Sidenav element
217
+ * @param {boolean} isOpen - Whether sidenav is open
218
+ */
219
+ handlePushVariant: function(sidenav, isOpen) {
220
+ // Find the main content wrapper
221
+ const content = document.querySelector('main, .main-content, .content, [role="main"]') || document.body;
222
+
223
+ if (isOpen) {
224
+ if (window.innerWidth >= this.breakpoint) {
225
+ if (this.isRightVariant(sidenav)) {
226
+ content.style.marginRight = sidenav.offsetWidth + 'px';
227
+ } else {
228
+ content.style.marginLeft = sidenav.offsetWidth + 'px';
229
+ }
230
+ }
231
+ } else {
232
+ content.style.marginLeft = '';
233
+ content.style.marginRight = '';
234
+ }
235
+ },
236
+
237
+ /**
238
+ * Handle window resize
239
+ */
240
+ handleResize: function() {
241
+ this.sidenavs.forEach(({ overlay }, sidenav) => {
242
+ // Close overlay sidenavs on resize to desktop if they're open
243
+ if (window.innerWidth >= this.breakpoint) {
244
+ if (this.isFixedVariant(sidenav) && !sidenav.classList.contains('is-open')) {
245
+ // Fixed sidenavs should be visible on desktop
246
+ sidenav.classList.add('is-open');
247
+ sidenav.setAttribute('aria-hidden', 'false');
248
+ overlay.classList.remove('is-visible');
249
+ }
250
+ } else {
251
+ // On mobile, fixed sidenavs become overlay
252
+ if (this.isFixedVariant(sidenav) && sidenav.classList.contains('is-open')) {
253
+ this.close(sidenav);
254
+ }
255
+ }
256
+ });
257
+ },
258
+
259
+ /**
260
+ * Destroy a sidenav instance and clean up event listeners
261
+ * @param {HTMLElement} sidenav - Sidenav element
262
+ */
263
+ destroy: function(sidenav) {
264
+ const data = this.sidenavs.get(sidenav);
265
+ if (!data) return;
266
+
267
+ // Close if open
268
+ if (sidenav.classList.contains('is-open')) {
269
+ this.close(sidenav);
270
+ }
271
+
272
+ data.cleanup.forEach(fn => fn());
273
+
274
+ // Remove created overlay
275
+ if (data.overlay && data.overlay.parentNode) {
276
+ data.overlay.parentNode.removeChild(data.overlay);
277
+ }
278
+
279
+ this.sidenavs.delete(sidenav);
280
+ },
281
+
282
+ /**
283
+ * Destroy all sidenav instances
284
+ */
285
+ destroyAll: function() {
286
+ this.sidenavs.forEach((data, sidenav) => {
287
+ this.destroy(sidenav);
288
+ });
289
+ this._globalCleanups.forEach(fn => fn());
290
+ this._globalCleanups = [];
291
+ }
292
+ };
293
+
294
+ // Register with Vanduo framework if available
295
+ if (typeof window.Vanduo !== 'undefined') {
296
+ window.Vanduo.register('sidenav', Sidenav);
297
+ }
298
+
299
+ // Expose globally
300
+ window.VanduoSidenav = Sidenav;
301
+
302
+ })();
303
+
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Vanduo Framework - Tabs Component
3
+ * Tabbed content navigation with keyboard support
4
+ */
5
+
6
+ (function() {
7
+ 'use strict';
8
+
9
+ /**
10
+ * Tabs Component
11
+ */
12
+ const Tabs = {
13
+ // Store initialized tab containers and their cleanup functions
14
+ instances: new Map(),
15
+
16
+ /**
17
+ * Initialize all tab components
18
+ */
19
+ init: function() {
20
+ const tabContainers = document.querySelectorAll('.vd-tabs, [data-tabs]');
21
+
22
+ tabContainers.forEach(container => {
23
+ if (this.instances.has(container)) {
24
+ return;
25
+ }
26
+ this.initTabs(container);
27
+ });
28
+ },
29
+
30
+ /**
31
+ * Initialize a single tab container
32
+ * @param {HTMLElement} container - Tabs container element
33
+ */
34
+ initTabs: function(container) {
35
+ const tabList = container.querySelector('.vd-tab-list, [role="tablist"]');
36
+ const tabLinks = container.querySelectorAll('.vd-tab-link, [data-tab]');
37
+ const tabPanes = container.querySelectorAll('.vd-tab-pane, [data-tab-pane]');
38
+
39
+ if (!tabList || tabLinks.length === 0) return;
40
+
41
+ const cleanupFunctions = [];
42
+
43
+ // Set up ARIA attributes
44
+ tabList.setAttribute('role', 'tablist');
45
+
46
+ tabLinks.forEach((link, index) => {
47
+ const tabId = link.dataset.tab || link.getAttribute('href')?.replace('#', '') || `tab-${index}`;
48
+ const pane = this.findPane(container, tabId, tabPanes);
49
+
50
+ // Set up tab attributes
51
+ link.setAttribute('role', 'tab');
52
+ link.setAttribute('aria-selected', link.classList.contains('is-active') ? 'true' : 'false');
53
+ link.setAttribute('tabindex', link.classList.contains('is-active') ? '0' : '-1');
54
+
55
+ if (!link.id) {
56
+ link.id = `tab-btn-${tabId}`;
57
+ }
58
+
59
+ // Set up pane attributes
60
+ if (pane) {
61
+ pane.setAttribute('role', 'tabpanel');
62
+ pane.setAttribute('aria-labelledby', link.id);
63
+ if (!pane.id) {
64
+ pane.id = `tab-pane-${tabId}`;
65
+ }
66
+ link.setAttribute('aria-controls', pane.id);
67
+ }
68
+
69
+ // Click handler
70
+ const clickHandler = (e) => {
71
+ e.preventDefault();
72
+ if (!link.classList.contains('disabled') && !link.disabled) {
73
+ this.activateTab(container, link, tabLinks, tabPanes);
74
+ }
75
+ };
76
+ link.addEventListener('click', clickHandler);
77
+ cleanupFunctions.push(() => link.removeEventListener('click', clickHandler));
78
+
79
+ // Keyboard navigation
80
+ const keydownHandler = (e) => {
81
+ this.handleKeydown(e, container, link, tabLinks, tabPanes);
82
+ };
83
+ link.addEventListener('keydown', keydownHandler);
84
+ cleanupFunctions.push(() => link.removeEventListener('keydown', keydownHandler));
85
+ });
86
+
87
+ // Ensure one tab is active
88
+ const activeTab = container.querySelector('.vd-tab-link.is-active, [data-tab].is-active');
89
+ if (!activeTab && tabLinks.length > 0) {
90
+ this.activateTab(container, tabLinks[0], tabLinks, tabPanes);
91
+ }
92
+
93
+ this.instances.set(container, { cleanup: cleanupFunctions });
94
+ },
95
+
96
+ /**
97
+ * Find the pane associated with a tab
98
+ * @param {HTMLElement} container - Tabs container
99
+ * @param {string} tabId - Tab identifier
100
+ * @param {NodeList} tabPanes - All tab panes
101
+ * @returns {HTMLElement|null} The matching pane
102
+ */
103
+ findPane: function(container, tabId, tabPanes) {
104
+ // Try data attribute first
105
+ let pane = container.querySelector(`[data-tab-pane="${tabId}"]`);
106
+
107
+ // Try ID
108
+ if (!pane) {
109
+ pane = container.querySelector(`#${tabId}`);
110
+ }
111
+
112
+ // Try matching by index
113
+ if (!pane) {
114
+ const tabLinks = container.querySelectorAll('.vd-tab-link, [data-tab]');
115
+ tabLinks.forEach((link, index) => {
116
+ const linkTabId = link.dataset.tab || link.getAttribute('href')?.replace('#', '');
117
+ if (linkTabId === tabId && tabPanes[index]) {
118
+ pane = tabPanes[index];
119
+ }
120
+ });
121
+ }
122
+
123
+ return pane;
124
+ },
125
+
126
+ /**
127
+ * Activate a tab
128
+ * @param {HTMLElement} container - Tabs container
129
+ * @param {HTMLElement} tab - Tab to activate
130
+ * @param {NodeList} allTabs - All tab links
131
+ * @param {NodeList} allPanes - All tab panes
132
+ */
133
+ activateTab: function(container, tab, allTabs, allPanes) {
134
+ const tabId = tab.dataset.tab || tab.getAttribute('href')?.replace('#', '') || tab.id;
135
+
136
+ // Deactivate all tabs
137
+ allTabs.forEach(t => {
138
+ t.classList.remove('is-active');
139
+ t.setAttribute('aria-selected', 'false');
140
+ t.setAttribute('tabindex', '-1');
141
+
142
+ // Also handle parent li if exists
143
+ if (t.parentElement && t.parentElement.classList.contains('tab-item')) {
144
+ t.parentElement.classList.remove('is-active');
145
+ }
146
+ });
147
+
148
+ // Hide all panes
149
+ allPanes.forEach(p => {
150
+ p.classList.remove('is-active');
151
+ });
152
+
153
+ // Activate selected tab
154
+ tab.classList.add('is-active');
155
+ tab.setAttribute('aria-selected', 'true');
156
+ tab.setAttribute('tabindex', '0');
157
+
158
+ // Also handle parent li if exists
159
+ if (tab.parentElement && tab.parentElement.classList.contains('tab-item')) {
160
+ tab.parentElement.classList.add('is-active');
161
+ }
162
+
163
+ // Show corresponding pane
164
+ const pane = this.findPane(container, tabId, allPanes);
165
+ if (pane) {
166
+ pane.classList.add('is-active');
167
+ }
168
+
169
+ // Dispatch custom event
170
+ const event = new CustomEvent('tab:change', {
171
+ bubbles: true,
172
+ detail: {
173
+ tab: tab,
174
+ pane: pane,
175
+ tabId: tabId
176
+ }
177
+ });
178
+ container.dispatchEvent(event);
179
+ },
180
+
181
+ /**
182
+ * Handle keyboard navigation
183
+ * @param {KeyboardEvent} e - Keyboard event
184
+ * @param {HTMLElement} container - Tabs container
185
+ * @param {HTMLElement} currentTab - Currently focused tab
186
+ * @param {NodeList} allTabs - All tab links
187
+ * @param {NodeList} allPanes - All tab panes
188
+ */
189
+ handleKeydown: function(e, container, currentTab, allTabs, allPanes) {
190
+ const isVertical = container.classList.contains('tabs-vertical');
191
+ const tabs = Array.from(allTabs).filter(t => !t.classList.contains('disabled') && !t.disabled);
192
+ const currentIndex = tabs.indexOf(currentTab);
193
+
194
+ let newIndex = currentIndex;
195
+
196
+ switch (e.key) {
197
+ case 'ArrowLeft':
198
+ if (!isVertical) {
199
+ e.preventDefault();
200
+ newIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;
201
+ }
202
+ break;
203
+
204
+ case 'ArrowRight':
205
+ if (!isVertical) {
206
+ e.preventDefault();
207
+ newIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0;
208
+ }
209
+ break;
210
+
211
+ case 'ArrowUp':
212
+ if (isVertical) {
213
+ e.preventDefault();
214
+ newIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;
215
+ }
216
+ break;
217
+
218
+ case 'ArrowDown':
219
+ if (isVertical) {
220
+ e.preventDefault();
221
+ newIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0;
222
+ }
223
+ break;
224
+
225
+ case 'Home':
226
+ e.preventDefault();
227
+ newIndex = 0;
228
+ break;
229
+
230
+ case 'End':
231
+ e.preventDefault();
232
+ newIndex = tabs.length - 1;
233
+ break;
234
+
235
+ case 'Enter':
236
+ case ' ':
237
+ e.preventDefault();
238
+ this.activateTab(container, currentTab, allTabs, allPanes);
239
+ return;
240
+
241
+ default:
242
+ return;
243
+ }
244
+
245
+ // Focus and activate new tab
246
+ if (newIndex !== currentIndex) {
247
+ tabs[newIndex].focus();
248
+ this.activateTab(container, tabs[newIndex], allTabs, allPanes);
249
+ }
250
+ },
251
+
252
+ /**
253
+ * Programmatically show a tab
254
+ * @param {string|HTMLElement} tab - Tab identifier or element
255
+ */
256
+ show: function(tab) {
257
+ let tabElement;
258
+
259
+ if (typeof tab === 'string') {
260
+ tabElement = document.querySelector(`[data-tab="${tab}"], [href="#${tab}"]`);
261
+ } else {
262
+ tabElement = tab;
263
+ }
264
+
265
+ if (!tabElement) return;
266
+
267
+ const container = tabElement.closest('.vd-tabs, [data-tabs]');
268
+ if (!container) return;
269
+
270
+ const allTabs = container.querySelectorAll('.vd-tab-link, [data-tab]');
271
+ const allPanes = container.querySelectorAll('.vd-tab-pane, [data-tab-pane]');
272
+
273
+ this.activateTab(container, tabElement, allTabs, allPanes);
274
+ },
275
+
276
+ /**
277
+ * Destroy a tabs instance and clean up event listeners
278
+ * @param {HTMLElement} container - Tabs container
279
+ */
280
+ destroy: function(container) {
281
+ const instance = this.instances.get(container);
282
+ if (!instance) return;
283
+
284
+ instance.cleanup.forEach(fn => fn());
285
+ this.instances.delete(container);
286
+ },
287
+
288
+ /**
289
+ * Destroy all tabs instances
290
+ */
291
+ destroyAll: function() {
292
+ this.instances.forEach((instance, container) => {
293
+ this.destroy(container);
294
+ });
295
+ }
296
+ };
297
+
298
+ // Register with Vanduo framework if available
299
+ if (typeof window.Vanduo !== 'undefined') {
300
+ window.Vanduo.register('tabs', Tabs);
301
+ }
302
+
303
+ })();