vanduo-framework 1.1.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 (196) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +205 -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,362 @@
1
+ /**
2
+ * Vanduo Framework - Dropdown Component
3
+ * JavaScript functionality for dropdown menus
4
+ */
5
+
6
+ (function() {
7
+ 'use strict';
8
+
9
+ /**
10
+ * Dropdown Component
11
+ */
12
+ const Dropdown = {
13
+ // Store initialized dropdowns and their cleanup functions
14
+ instances: new Map(),
15
+ // Typeahead state
16
+ _typeaheadBuffer: '',
17
+ _typeaheadTimer: null,
18
+
19
+ /**
20
+ * Initialize dropdown components
21
+ */
22
+ init: function() {
23
+ const dropdowns = document.querySelectorAll('.vd-dropdown');
24
+
25
+ dropdowns.forEach(dropdown => {
26
+ if (this.instances.has(dropdown)) {
27
+ return;
28
+ }
29
+ this.initDropdown(dropdown);
30
+ });
31
+ },
32
+
33
+ /**
34
+ * Initialize a single dropdown
35
+ * @param {HTMLElement} dropdown - Dropdown container
36
+ */
37
+ initDropdown: function(dropdown) {
38
+ const toggle = dropdown.querySelector('.vd-dropdown-toggle');
39
+ const menu = dropdown.querySelector('.vd-dropdown-menu');
40
+
41
+ if (!toggle || !menu) {
42
+ return;
43
+ }
44
+
45
+ const cleanupFunctions = [];
46
+
47
+ // Set ARIA attributes
48
+ toggle.setAttribute('aria-haspopup', 'true');
49
+ toggle.setAttribute('aria-expanded', 'false');
50
+ menu.setAttribute('role', 'menu');
51
+ menu.setAttribute('aria-hidden', 'true');
52
+
53
+ // Toggle on click
54
+ const toggleClickHandler = (e) => {
55
+ e.preventDefault();
56
+ e.stopPropagation();
57
+ this.toggleDropdown(dropdown, toggle, menu);
58
+ };
59
+ toggle.addEventListener('click', toggleClickHandler);
60
+ cleanupFunctions.push(() => toggle.removeEventListener('click', toggleClickHandler));
61
+
62
+ // Close on outside click
63
+ const documentClickHandler = (e) => {
64
+ if (!dropdown.contains(e.target) && menu.classList.contains('is-open')) {
65
+ this.closeDropdown(dropdown, toggle, menu);
66
+ }
67
+ };
68
+ document.addEventListener('click', documentClickHandler);
69
+ cleanupFunctions.push(() => document.removeEventListener('click', documentClickHandler));
70
+
71
+ // Keyboard navigation
72
+ const keydownHandler = (e) => {
73
+ this.handleKeydown(e, dropdown, toggle, menu);
74
+ };
75
+ toggle.addEventListener('keydown', keydownHandler);
76
+ cleanupFunctions.push(() => toggle.removeEventListener('keydown', keydownHandler));
77
+
78
+ // Handle item clicks
79
+ const items = menu.querySelectorAll('.vd-dropdown-item:not(.disabled):not(.is-disabled)');
80
+ items.forEach(item => {
81
+ const itemClickHandler = (e) => {
82
+ e.preventDefault();
83
+ this.selectItem(item, dropdown, toggle, menu);
84
+ };
85
+ item.addEventListener('click', itemClickHandler);
86
+ cleanupFunctions.push(() => item.removeEventListener('click', itemClickHandler));
87
+
88
+ const itemKeydownHandler = (e) => {
89
+ if (e.key === 'Enter' || e.key === ' ') {
90
+ e.preventDefault();
91
+ this.selectItem(item, dropdown, toggle, menu);
92
+ }
93
+ };
94
+ item.addEventListener('keydown', itemKeydownHandler);
95
+ cleanupFunctions.push(() => item.removeEventListener('keydown', itemKeydownHandler));
96
+ });
97
+
98
+ this.instances.set(dropdown, { toggle, menu, cleanup: cleanupFunctions });
99
+ },
100
+
101
+ /**
102
+ * Toggle dropdown
103
+ * @param {HTMLElement} dropdown - Dropdown container
104
+ * @param {HTMLElement} toggle - Toggle button
105
+ * @param {HTMLElement} menu - Dropdown menu
106
+ */
107
+ toggleDropdown: function(dropdown, toggle, menu) {
108
+ const isOpen = menu.classList.contains('is-open');
109
+
110
+ if (isOpen) {
111
+ this.closeDropdown(dropdown, toggle, menu);
112
+ } else {
113
+ this.openDropdown(dropdown, toggle, menu);
114
+ }
115
+ },
116
+
117
+ /**
118
+ * Open dropdown
119
+ * @param {HTMLElement} dropdown - Dropdown container
120
+ * @param {HTMLElement} toggle - Toggle button
121
+ * @param {HTMLElement} menu - Dropdown menu
122
+ */
123
+ openDropdown: function(dropdown, toggle, menu) {
124
+ // Close other open dropdowns
125
+ const otherOpen = document.querySelectorAll('.vd-dropdown-menu.is-open');
126
+ otherOpen.forEach(otherMenu => {
127
+ if (otherMenu !== menu) {
128
+ const otherDropdown = otherMenu.closest('.vd-dropdown');
129
+ const otherToggle = otherDropdown.querySelector('.vd-dropdown-toggle');
130
+ this.closeDropdown(otherDropdown, otherToggle, otherMenu);
131
+ }
132
+ });
133
+
134
+ dropdown.classList.add('is-open');
135
+ menu.classList.add('is-open');
136
+ toggle.setAttribute('aria-expanded', 'true');
137
+ menu.setAttribute('aria-hidden', 'false');
138
+
139
+ // Position menu
140
+ this.positionMenu(dropdown, menu);
141
+
142
+ // Focus first item
143
+ const firstItem = menu.querySelector('.vd-dropdown-item:not(.disabled):not(.is-disabled)');
144
+ if (firstItem) {
145
+ setTimeout(() => firstItem.focus(), 0);
146
+ }
147
+ },
148
+
149
+ /**
150
+ * Close dropdown
151
+ * @param {HTMLElement} dropdown - Dropdown container
152
+ * @param {HTMLElement} toggle - Toggle button
153
+ * @param {HTMLElement} menu - Dropdown menu
154
+ */
155
+ closeDropdown: function(dropdown, toggle, menu) {
156
+ dropdown.classList.remove('is-open');
157
+ menu.classList.remove('is-open');
158
+ toggle.setAttribute('aria-expanded', 'false');
159
+ menu.setAttribute('aria-hidden', 'true');
160
+
161
+ // Return focus to toggle
162
+ toggle.focus();
163
+ },
164
+
165
+ /**
166
+ * Position dropdown menu
167
+ * @param {HTMLElement} dropdown - Dropdown container
168
+ * @param {HTMLElement} menu - Dropdown menu
169
+ */
170
+ positionMenu: function(dropdown, menu) {
171
+ const rect = dropdown.getBoundingClientRect();
172
+ const menuRect = menu.getBoundingClientRect();
173
+ const viewportWidth = window.innerWidth;
174
+ const viewportHeight = window.innerHeight;
175
+ const padding = 8;
176
+
177
+ // Check if menu overflows right
178
+ if (rect.left + menuRect.width > viewportWidth - padding) {
179
+ menu.classList.add('vd-dropdown-menu-end');
180
+ menu.classList.remove('vd-dropdown-menu-start');
181
+ }
182
+
183
+ // Check if menu overflows bottom (for top positioning)
184
+ if (menu.classList.contains('dropdown-menu-top')) {
185
+ if (rect.top - menuRect.height < padding) {
186
+ menu.classList.remove('vd-dropdown-menu-top');
187
+ }
188
+ } else {
189
+ if (rect.bottom + menuRect.height > viewportHeight - padding) {
190
+ menu.classList.add('vd-dropdown-menu-top');
191
+ }
192
+ }
193
+ },
194
+
195
+ /**
196
+ * Handle keyboard navigation
197
+ * @param {KeyboardEvent} e - Keyboard event
198
+ * @param {HTMLElement} dropdown - Dropdown container
199
+ * @param {HTMLElement} toggle - Toggle button
200
+ * @param {HTMLElement} menu - Dropdown menu
201
+ */
202
+ handleKeydown: function(e, dropdown, toggle, menu) {
203
+ const isOpen = menu.classList.contains('is-open');
204
+ const items = Array.from(menu.querySelectorAll('.vd-dropdown-item:not(.disabled):not(.is-disabled)'));
205
+ const currentIndex = items.findIndex(item => item === document.activeElement);
206
+
207
+ switch (e.key) {
208
+ case 'Enter':
209
+ case ' ':
210
+ case 'ArrowDown':
211
+ e.preventDefault();
212
+ if (!isOpen) {
213
+ this.openDropdown(dropdown, toggle, menu);
214
+ } else if (e.key === 'ArrowDown') {
215
+ const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
216
+ items[nextIndex].focus();
217
+ }
218
+ break;
219
+
220
+ case 'ArrowUp':
221
+ if (isOpen) {
222
+ e.preventDefault();
223
+ const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
224
+ items[prevIndex].focus();
225
+ }
226
+ break;
227
+
228
+ case 'Escape':
229
+ if (isOpen) {
230
+ e.preventDefault();
231
+ this.closeDropdown(dropdown, toggle, menu);
232
+ }
233
+ break;
234
+
235
+ case 'Home':
236
+ if (isOpen) {
237
+ e.preventDefault();
238
+ items[0].focus();
239
+ }
240
+ break;
241
+
242
+ case 'End':
243
+ if (isOpen) {
244
+ e.preventDefault();
245
+ items[items.length - 1].focus();
246
+ }
247
+ break;
248
+
249
+ default:
250
+ // Typeahead: jump to matching item when typing printable characters
251
+ if (isOpen && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
252
+ clearTimeout(this._typeaheadTimer);
253
+ this._typeaheadBuffer += e.key.toLowerCase();
254
+
255
+ const match = items.find(item =>
256
+ item.textContent.trim().toLowerCase().startsWith(this._typeaheadBuffer)
257
+ );
258
+ if (match) {
259
+ match.focus();
260
+ }
261
+
262
+ this._typeaheadTimer = setTimeout(() => {
263
+ this._typeaheadBuffer = '';
264
+ }, 500);
265
+ }
266
+ break;
267
+ }
268
+ },
269
+
270
+ /**
271
+ * Select dropdown item
272
+ * @param {HTMLElement} item - Dropdown item
273
+ * @param {HTMLElement} dropdown - Dropdown container
274
+ * @param {HTMLElement} toggle - Toggle button
275
+ * @param {HTMLElement} menu - Dropdown menu
276
+ */
277
+ selectItem: function(item, dropdown, toggle, menu) {
278
+ // Remove active from all items
279
+ menu.querySelectorAll('.vd-dropdown-item').forEach(i => {
280
+ i.classList.remove('active', 'is-active');
281
+ });
282
+
283
+ // Add active to selected item
284
+ item.classList.add('active', 'is-active');
285
+
286
+ // Update toggle text if it's a button
287
+ if (toggle.tagName === 'BUTTON' || toggle.classList.contains('btn')) {
288
+ toggle.textContent = item.textContent.trim();
289
+ }
290
+
291
+ // Close dropdown
292
+ this.closeDropdown(dropdown, toggle, menu);
293
+
294
+ // Dispatch event
295
+ item.dispatchEvent(new CustomEvent('dropdown:select', {
296
+ bubbles: true,
297
+ detail: { item, value: item.dataset.value || item.textContent }
298
+ }));
299
+ },
300
+
301
+ /**
302
+ * Open dropdown programmatically
303
+ * @param {HTMLElement|string} dropdown - Dropdown container or selector
304
+ */
305
+ open: function(dropdown) {
306
+ const el = typeof dropdown === 'string' ? document.querySelector(dropdown) : dropdown;
307
+ if (el) {
308
+ const toggle = el.querySelector('.vd-dropdown-toggle');
309
+ const menu = el.querySelector('.vd-dropdown-menu');
310
+ if (toggle && menu) {
311
+ this.openDropdown(el, toggle, menu);
312
+ }
313
+ }
314
+ },
315
+
316
+ /**
317
+ * Close dropdown programmatically
318
+ * @param {HTMLElement|string} dropdown - Dropdown container or selector
319
+ */
320
+ close: function(dropdown) {
321
+ const el = typeof dropdown === 'string' ? document.querySelector(dropdown) : dropdown;
322
+ if (el) {
323
+ const toggle = el.querySelector('.vd-dropdown-toggle');
324
+ const menu = el.querySelector('.vd-dropdown-menu');
325
+ if (toggle && menu) {
326
+ this.closeDropdown(el, toggle, menu);
327
+ }
328
+ }
329
+ },
330
+
331
+ /**
332
+ * Destroy a dropdown instance and clean up event listeners
333
+ * @param {HTMLElement} dropdown - Dropdown element
334
+ */
335
+ destroy: function(dropdown) {
336
+ const instance = this.instances.get(dropdown);
337
+ if (!instance) return;
338
+
339
+ instance.cleanup.forEach(fn => fn());
340
+ this.instances.delete(dropdown);
341
+ },
342
+
343
+ /**
344
+ * Destroy all dropdown instances
345
+ */
346
+ destroyAll: function() {
347
+ this.instances.forEach((instance, dropdown) => {
348
+ this.destroy(dropdown);
349
+ });
350
+ }
351
+ };
352
+
353
+ // Register with Vanduo framework if available
354
+ if (typeof window.Vanduo !== 'undefined') {
355
+ window.Vanduo.register('dropdown', Dropdown);
356
+ }
357
+
358
+ // Expose globally
359
+ window.VanduoDropdown = Dropdown;
360
+
361
+ })();
362
+
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Vanduo Framework - Font Switcher
3
+ * Handles font selection and persistence for previewing different typefaces
4
+ */
5
+
6
+ (function() {
7
+ 'use strict';
8
+
9
+ const FontSwitcher = {
10
+ STORAGE_KEY: 'vanduo-font-preference',
11
+ isInitialized: false,
12
+
13
+ // Available fonts configuration
14
+ fonts: {
15
+ 'system': {
16
+ name: 'System Default',
17
+ family: null // Uses CSS default
18
+ },
19
+ 'inter': {
20
+ name: 'Inter',
21
+ family: "'Inter', sans-serif"
22
+ },
23
+ 'source-sans': {
24
+ name: 'Source Sans 3',
25
+ family: "'Source Sans 3', sans-serif"
26
+ },
27
+ 'fira-sans': {
28
+ name: 'Fira Sans',
29
+ family: "'Fira Sans', sans-serif"
30
+ },
31
+ 'ibm-plex': {
32
+ name: 'IBM Plex Sans',
33
+ family: "'IBM Plex Sans', sans-serif"
34
+ },
35
+ 'jetbrains-mono': {
36
+ name: 'JetBrains Mono',
37
+ family: "'JetBrains Mono', monospace"
38
+ },
39
+ 'ubuntu': {
40
+ name: 'Ubuntu',
41
+ family: "'Ubuntu', sans-serif",
42
+ category: 'sans-serif',
43
+ description: 'Friendly, humanist sans-serif'
44
+ },
45
+ 'open-sans': {
46
+ name: 'Open Sans',
47
+ family: "'Open Sans', sans-serif",
48
+ category: 'sans-serif',
49
+ description: 'Neutral, highly readable'
50
+ },
51
+ 'rubik': {
52
+ name: 'Rubik',
53
+ family: "'Rubik', sans-serif",
54
+ category: 'sans-serif',
55
+ description: 'Modern, geometric'
56
+ },
57
+ 'titillium-web': {
58
+ name: 'Titillium Web',
59
+ family: "'Titillium Web', sans-serif",
60
+ category: 'sans-serif',
61
+ description: 'Technical, elegant'
62
+ }
63
+ },
64
+
65
+ init: function() {
66
+ this.state = {
67
+ preference: this.getPreference()
68
+ };
69
+
70
+ if (this.isInitialized) {
71
+ this.applyFont();
72
+ this.renderUI();
73
+ this.updateUI();
74
+ return;
75
+ }
76
+
77
+ this.isInitialized = true;
78
+
79
+ this.applyFont();
80
+ this.renderUI();
81
+
82
+ console.log('Vanduo Font Switcher initialized');
83
+ },
84
+
85
+ /**
86
+ * Get saved font preference from localStorage
87
+ * @returns {string} Font key or 'ubuntu' (default)
88
+ */
89
+ getPreference: function() {
90
+ return this.getStorageValue(this.STORAGE_KEY, 'ubuntu');
91
+ },
92
+
93
+ /**
94
+ * Set font preference and apply it
95
+ * @param {string} fontKey - The font key to apply
96
+ */
97
+ setPreference: function(fontKey) {
98
+ if (!this.fonts[fontKey]) {
99
+ console.warn('Unknown font:', fontKey);
100
+ return;
101
+ }
102
+
103
+ this.state.preference = fontKey;
104
+ this.setStorageValue(this.STORAGE_KEY, fontKey);
105
+ this.applyFont();
106
+ this.updateUI();
107
+
108
+ // Dispatch custom event for other components to listen to
109
+ const event = new CustomEvent('font:change', {
110
+ bubbles: true,
111
+ detail: { font: fontKey, fontData: this.fonts[fontKey] }
112
+ });
113
+ document.dispatchEvent(event);
114
+ },
115
+
116
+ /**
117
+ * Apply the current font preference to the document
118
+ */
119
+ applyFont: function() {
120
+ const fontKey = this.state.preference;
121
+
122
+ if (fontKey === 'system') {
123
+ // Remove data-font attribute to use system default
124
+ document.documentElement.removeAttribute('data-font');
125
+ } else {
126
+ // Set data-font attribute which triggers CSS variable override
127
+ document.documentElement.setAttribute('data-font', fontKey);
128
+ }
129
+ },
130
+
131
+ /**
132
+ * Initialize UI elements with data-toggle="font"
133
+ */
134
+ renderUI: function() {
135
+ const toggles = document.querySelectorAll('[data-toggle="font"]');
136
+
137
+ toggles.forEach(toggle => {
138
+ if (toggle.getAttribute('data-font-initialized') === 'true') {
139
+ if (toggle.tagName === 'SELECT') {
140
+ toggle.value = this.state.preference;
141
+ }
142
+ return;
143
+ }
144
+
145
+ if (toggle.tagName === 'SELECT') {
146
+ // Set initial value
147
+ toggle.value = this.state.preference;
148
+
149
+ // Listen for changes
150
+ const onChange = (e) => {
151
+ this.setPreference(e.target.value);
152
+ };
153
+ toggle.addEventListener('change', onChange);
154
+ toggle._fontToggleHandler = onChange;
155
+ } else {
156
+ // Button implementation - cycle through fonts
157
+ const onClick = () => {
158
+ const fontKeys = Object.keys(this.fonts);
159
+ const currentIndex = fontKeys.indexOf(this.state.preference);
160
+ const nextIndex = (currentIndex + 1) % fontKeys.length;
161
+ this.setPreference(fontKeys[nextIndex]);
162
+ };
163
+ toggle.addEventListener('click', onClick);
164
+ toggle._fontToggleHandler = onClick;
165
+ }
166
+
167
+ toggle.setAttribute('data-font-initialized', 'true');
168
+ });
169
+ },
170
+
171
+ /**
172
+ * Update all UI elements to reflect current state
173
+ */
174
+ updateUI: function() {
175
+ const toggles = document.querySelectorAll('[data-toggle="font"]');
176
+
177
+ toggles.forEach(toggle => {
178
+ if (toggle.tagName === 'SELECT') {
179
+ toggle.value = this.state.preference;
180
+ } else {
181
+ // Update button text if it has a label span
182
+ const label = toggle.querySelector('.font-current-label');
183
+ if (label) {
184
+ label.textContent = this.fonts[this.state.preference].name;
185
+ }
186
+ }
187
+ });
188
+ },
189
+
190
+ /**
191
+ * Get the current font preference
192
+ * @returns {string} Current font key
193
+ */
194
+ getCurrentFont: function() {
195
+ return this.state.preference;
196
+ },
197
+
198
+ /**
199
+ * Get font data for a given key
200
+ * @param {string} fontKey - The font key
201
+ * @returns {Object|null} Font data or null
202
+ */
203
+ getFontData: function(fontKey) {
204
+ return this.fonts[fontKey] || null;
205
+ },
206
+
207
+ destroyAll: function() {
208
+ const toggles = document.querySelectorAll('[data-toggle="font"][data-font-initialized="true"]');
209
+ toggles.forEach(toggle => {
210
+ if (toggle._fontToggleHandler) {
211
+ const eventName = toggle.tagName === 'SELECT' ? 'change' : 'click';
212
+ toggle.removeEventListener(eventName, toggle._fontToggleHandler);
213
+ delete toggle._fontToggleHandler;
214
+ }
215
+ toggle.removeAttribute('data-font-initialized');
216
+ });
217
+
218
+ this.isInitialized = false;
219
+ },
220
+
221
+ getStorageValue: function(key, fallback) {
222
+ if (typeof window.safeStorageGet === 'function') {
223
+ return window.safeStorageGet(key, fallback);
224
+ }
225
+ try {
226
+ const value = localStorage.getItem(key);
227
+ return value !== null ? value : fallback;
228
+ } catch (_e) {
229
+ return fallback;
230
+ }
231
+ },
232
+
233
+ setStorageValue: function(key, value) {
234
+ if (typeof window.safeStorageSet === 'function') {
235
+ return window.safeStorageSet(key, value);
236
+ }
237
+ try {
238
+ localStorage.setItem(key, value);
239
+ return true;
240
+ } catch (_e) {
241
+ return false;
242
+ }
243
+ }
244
+ };
245
+
246
+ // Register component
247
+ if (window.Vanduo) {
248
+ window.Vanduo.register('fontSwitcher', FontSwitcher);
249
+ }
250
+
251
+ // Expose globally for convenience
252
+ window.FontSwitcher = FontSwitcher;
253
+ })();