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,279 @@
1
+ /**
2
+ * Vanduo Framework - Grid Layout Component
3
+ * Toggle between standard 12-column and Fibonacci grid modes
4
+ * via data-layout-mode attribute and toggle buttons
5
+ */
6
+
7
+ (function () {
8
+ 'use strict';
9
+
10
+ var supportsHas = (function () {
11
+ try {
12
+ return CSS.supports('selector(:has(*))');
13
+ } catch (_e) {
14
+ return false;
15
+ }
16
+ })();
17
+
18
+ /**
19
+ * Grid Layout Component
20
+ */
21
+ var GridLayout = {
22
+ instances: new Map(),
23
+
24
+ /**
25
+ * Initialize all grid layout containers
26
+ */
27
+ init: function () {
28
+ var containers = document.querySelectorAll('[data-layout-mode]');
29
+
30
+ containers.forEach(function (container) {
31
+ if (this.instances.has(container)) {
32
+ return;
33
+ }
34
+ this.initContainer(container);
35
+ }.bind(this));
36
+
37
+ this.initToggleButtons();
38
+ },
39
+
40
+ /**
41
+ * Initialize a single grid container
42
+ * @param {HTMLElement} container - Element with data-layout-mode
43
+ */
44
+ initContainer: function (container) {
45
+ var mode = container.getAttribute('data-layout-mode') || 'standard';
46
+ var cleanupFunctions = [];
47
+
48
+ this.applyMode(container, mode);
49
+
50
+ container.setAttribute('role', 'region');
51
+ container.setAttribute('aria-label', 'Grid layout: ' + mode + ' mode');
52
+
53
+ this.instances.set(container, {
54
+ cleanup: cleanupFunctions,
55
+ mode: mode
56
+ });
57
+ },
58
+
59
+ /**
60
+ * Initialize toggle buttons that target grid containers
61
+ */
62
+ initToggleButtons: function () {
63
+ var toggleButtons = document.querySelectorAll('[data-grid-toggle]');
64
+
65
+ toggleButtons.forEach(function (button) {
66
+ if (button.getAttribute('data-grid-initialized') === 'true') {
67
+ return;
68
+ }
69
+
70
+ var clickHandler = function (e) {
71
+ e.preventDefault();
72
+ var targetSelector = button.getAttribute('data-grid-toggle');
73
+ var target;
74
+
75
+ if (targetSelector) {
76
+ target = document.querySelector(targetSelector);
77
+ } else {
78
+ target = button.closest('[data-layout-mode]');
79
+ }
80
+
81
+ if (target) {
82
+ this.toggle(target);
83
+ }
84
+ }.bind(this);
85
+
86
+ button.addEventListener('click', clickHandler);
87
+ button.setAttribute('data-grid-initialized', 'true');
88
+ button.setAttribute('aria-pressed', 'false');
89
+
90
+ button._gridCleanup = function () {
91
+ button.removeEventListener('click', clickHandler);
92
+ button.removeAttribute('data-grid-initialized');
93
+ button.removeAttribute('aria-pressed');
94
+ };
95
+ }.bind(this));
96
+ },
97
+
98
+ /**
99
+ * Apply Fibonacci grid-template-columns inline for browsers without :has()
100
+ * @param {HTMLElement} container - Grid container
101
+ */
102
+ applyFibFallback: function (container) {
103
+ if (supportsHas) return;
104
+
105
+ var rows = container.querySelectorAll('.vd-row, .row');
106
+ rows.forEach(function (row) {
107
+ var cols = row.querySelectorAll(':scope > [class*="vd-col-"], :scope > [class*="col-"]');
108
+ var count = cols.length;
109
+
110
+ if (count === 1) {
111
+ row.style.gridTemplateColumns = '1fr';
112
+ } else if (count === 2) {
113
+ row.style.gridTemplateColumns = '1fr 1.618fr';
114
+ } else if (count === 3) {
115
+ row.style.gridTemplateColumns = '2fr 3fr 5fr';
116
+ } else if (count === 4) {
117
+ row.style.gridTemplateColumns = '1fr 2fr 3fr 5fr';
118
+ } else {
119
+ row.style.gridTemplateColumns = 'repeat(' + count + ', 1fr)';
120
+ }
121
+ });
122
+ },
123
+
124
+ /**
125
+ * Remove inline grid-template-columns fallback
126
+ * @param {HTMLElement} container - Grid container
127
+ */
128
+ removeFibFallback: function (container) {
129
+ var rows = container.querySelectorAll('.vd-row, .row');
130
+ rows.forEach(function (row) {
131
+ row.style.gridTemplateColumns = '';
132
+ });
133
+ },
134
+
135
+ /**
136
+ * Apply a layout mode to a container
137
+ * @param {HTMLElement} container - Target container
138
+ * @param {string} mode - 'fibonacci' or 'standard'
139
+ */
140
+ applyMode: function (container, mode) {
141
+ container.classList.remove('vd-grid-standard', 'vd-grid-fibonacci');
142
+
143
+ if (mode === 'fibonacci') {
144
+ container.classList.add('vd-grid-fibonacci');
145
+ this.applyFibFallback(container);
146
+ } else {
147
+ container.classList.add('vd-grid-standard');
148
+ this.removeFibFallback(container);
149
+ }
150
+
151
+ container.setAttribute('data-layout-mode', mode);
152
+ container.setAttribute('aria-label', 'Grid layout: ' + mode + ' mode');
153
+
154
+ // Update associated toggle button states
155
+ var toggleButtons = document.querySelectorAll('[data-grid-toggle]');
156
+ toggleButtons.forEach(function (btn) {
157
+ var targetSelector = btn.getAttribute('data-grid-toggle');
158
+ if (targetSelector && container.matches(targetSelector)) {
159
+ var isActive = (mode === 'fibonacci');
160
+ if (isActive) {
161
+ btn.classList.add('is-active');
162
+ } else {
163
+ btn.classList.remove('is-active');
164
+ }
165
+ btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
166
+ }
167
+ });
168
+
169
+ // Store mode in instance
170
+ var instance = this.instances.get(container);
171
+ if (instance) {
172
+ instance.mode = mode;
173
+ }
174
+
175
+ // Dispatch custom event
176
+ var event;
177
+ try {
178
+ event = new CustomEvent('grid:modechange', {
179
+ bubbles: true,
180
+ detail: {
181
+ container: container,
182
+ mode: mode
183
+ }
184
+ });
185
+ } catch (_e) {
186
+ event = document.createEvent('CustomEvent');
187
+ event.initCustomEvent('grid:modechange', true, true, {
188
+ container: container,
189
+ mode: mode
190
+ });
191
+ }
192
+ container.dispatchEvent(event);
193
+ },
194
+
195
+ /**
196
+ * Toggle between standard and fibonacci modes
197
+ * @param {HTMLElement|string} container - Container element or selector
198
+ */
199
+ toggle: function (container) {
200
+ if (typeof container === 'string') {
201
+ container = document.querySelector(container);
202
+ }
203
+ if (!container) return;
204
+
205
+ var currentMode = container.getAttribute('data-layout-mode') || 'standard';
206
+ var newMode = (currentMode === 'fibonacci') ? 'standard' : 'fibonacci';
207
+ this.applyMode(container, newMode);
208
+ },
209
+
210
+ /**
211
+ * Set a specific mode
212
+ * @param {HTMLElement|string} container - Container element or selector
213
+ * @param {string} mode - 'fibonacci' or 'standard'
214
+ */
215
+ setMode: function (container, mode) {
216
+ if (typeof container === 'string') {
217
+ container = document.querySelector(container);
218
+ }
219
+ if (!container) return;
220
+ if (mode !== 'fibonacci' && mode !== 'standard') return;
221
+
222
+ this.applyMode(container, mode);
223
+ },
224
+
225
+ /**
226
+ * Get the current mode of a container
227
+ * @param {HTMLElement|string} container - Container element or selector
228
+ * @returns {string|null} Current mode or null
229
+ */
230
+ getMode: function (container) {
231
+ if (typeof container === 'string') {
232
+ container = document.querySelector(container);
233
+ }
234
+ if (!container) return null;
235
+ return container.getAttribute('data-layout-mode') || 'standard';
236
+ },
237
+
238
+ /**
239
+ * Destroy a single grid layout instance
240
+ * @param {HTMLElement} container - Grid container
241
+ */
242
+ destroy: function (container) {
243
+ var instance = this.instances.get(container);
244
+ if (!instance) return;
245
+
246
+ instance.cleanup.forEach(function (fn) { fn(); });
247
+ container.classList.remove('vd-grid-standard', 'vd-grid-fibonacci');
248
+ container.removeAttribute('aria-label');
249
+ this.removeFibFallback(container);
250
+ this.instances.delete(container);
251
+ },
252
+
253
+ /**
254
+ * Destroy all grid layout instances and clean up toggle buttons
255
+ */
256
+ destroyAll: function () {
257
+ this.instances.forEach(function (instance, container) {
258
+ this.destroy(container);
259
+ }.bind(this));
260
+
261
+ var toggleButtons = document.querySelectorAll('[data-grid-initialized="true"]');
262
+ toggleButtons.forEach(function (button) {
263
+ if (button._gridCleanup) {
264
+ button._gridCleanup();
265
+ delete button._gridCleanup;
266
+ }
267
+ });
268
+ }
269
+ };
270
+
271
+ // Register with Vanduo framework
272
+ if (typeof window.Vanduo !== 'undefined') {
273
+ window.Vanduo.register('gridLayout', GridLayout);
274
+ }
275
+
276
+ // Expose globally
277
+ window.VanduoGridLayout = GridLayout;
278
+
279
+ })();
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Vanduo Framework - Image Box Component
3
+ * Lightbox-style image enlargement with smooth transitions
4
+ *
5
+ * Features:
6
+ * - Click to enlarge images with data-image-box attribute
7
+ * - Smooth scale and opacity transitions
8
+ * - Dismiss via click, ESC key, or scroll
9
+ * - Magnifying glass cursor on hover
10
+ * - Accessible with ARIA attributes
11
+ * - Reduced motion support
12
+ */
13
+
14
+ (function () {
15
+ 'use strict';
16
+
17
+ /**
18
+ * Image Box Component
19
+ */
20
+ const ImageBox = {
21
+ backdrop: null,
22
+ container: null,
23
+ img: null,
24
+ closeBtn: null,
25
+ caption: null,
26
+ currentTrigger: null,
27
+ scrollThreshold: 50,
28
+ initialScrollY: 0,
29
+ isOpen: false,
30
+
31
+ // Store cleanup functions for event listeners
32
+ _cleanupFunctions: [],
33
+
34
+ /**
35
+ * Initialize Image Box component
36
+ */
37
+ init: function () {
38
+ this.createBackdrop();
39
+ this.bindTriggers();
40
+ },
41
+
42
+ /**
43
+ * Create backdrop elements
44
+ */
45
+ createBackdrop: function () {
46
+ // Prevent duplicate backdrop creation
47
+ if (this.backdrop || document.querySelector('.vd-image-box-backdrop')) {
48
+ // If backdrop already exists in DOM, reuse it
49
+ if (!this.backdrop) {
50
+ this.backdrop = document.querySelector('.vd-image-box-backdrop');
51
+ this.container = this.backdrop.querySelector('.vd-image-box-container');
52
+ this.img = this.backdrop.querySelector('.vd-image-box-img');
53
+ this.closeBtn = this.backdrop.querySelector('.vd-image-box-close');
54
+ this.caption = this.backdrop.querySelector('.vd-image-box-caption');
55
+ this.bindBackdropEvents();
56
+ }
57
+ return;
58
+ }
59
+
60
+ // Create backdrop
61
+ this.backdrop = document.createElement('div');
62
+ this.backdrop.className = 'vd-image-box-backdrop';
63
+ this.backdrop.setAttribute('role', 'dialog');
64
+ this.backdrop.setAttribute('aria-modal', 'true');
65
+ this.backdrop.setAttribute('aria-label', 'Image viewer');
66
+ this.backdrop.setAttribute('tabindex', '-1');
67
+
68
+ // Create container
69
+ this.container = document.createElement('div');
70
+ this.container.className = 'vd-image-box-container';
71
+
72
+ // Create image
73
+ this.img = document.createElement('img');
74
+ this.img.className = 'vd-image-box-img';
75
+ this.img.alt = '';
76
+
77
+ // Create close button
78
+ this.closeBtn = document.createElement('button');
79
+ this.closeBtn.className = 'vd-image-box-close';
80
+ this.closeBtn.setAttribute('aria-label', 'Close image viewer');
81
+ this.closeBtn.innerHTML = '×';
82
+
83
+ // Create caption element
84
+ this.caption = document.createElement('div');
85
+ this.caption.className = 'vd-image-box-caption';
86
+
87
+ // Assemble
88
+ this.container.appendChild(this.img);
89
+ this.backdrop.appendChild(this.closeBtn);
90
+ this.backdrop.appendChild(this.container);
91
+ this.backdrop.appendChild(this.caption);
92
+ document.body.appendChild(this.backdrop);
93
+
94
+ // Bind backdrop events
95
+ this.bindBackdropEvents();
96
+ },
97
+
98
+ /**
99
+ * Bind events to backdrop elements
100
+ */
101
+ bindBackdropEvents: function () {
102
+ const self = this;
103
+
104
+ // Close on backdrop click (but not when clicking the image)
105
+ const backdropClickHandler = function (e) {
106
+ if (e.target === self.backdrop || e.target === self.container) {
107
+ self.close();
108
+ }
109
+ };
110
+ this.backdrop.addEventListener('click', backdropClickHandler);
111
+ this._cleanupFunctions.push(() => this.backdrop.removeEventListener('click', backdropClickHandler));
112
+
113
+ // Close on image click
114
+ const imgClickHandler = function () {
115
+ self.close();
116
+ };
117
+ this.img.addEventListener('click', imgClickHandler);
118
+ this._cleanupFunctions.push(() => this.img.removeEventListener('click', imgClickHandler));
119
+
120
+ // Close on close button click
121
+ const closeBtnHandler = function () {
122
+ self.close();
123
+ };
124
+ this.closeBtn.addEventListener('click', closeBtnHandler);
125
+ this._cleanupFunctions.push(() => this.closeBtn.removeEventListener('click', closeBtnHandler));
126
+
127
+ // ESC key handler
128
+ const escHandler = function (e) {
129
+ if (e.key === 'Escape' && self.isOpen) {
130
+ self.close();
131
+ }
132
+ };
133
+ document.addEventListener('keydown', escHandler);
134
+ this._cleanupFunctions.push(() => document.removeEventListener('keydown', escHandler));
135
+
136
+ // Scroll handler for dismissal
137
+ const scrollHandler = function () {
138
+ if (!self.isOpen) return;
139
+
140
+ const currentScrollY = window.scrollY;
141
+ const scrollDelta = Math.abs(currentScrollY - self.initialScrollY);
142
+
143
+ if (scrollDelta > self.scrollThreshold) {
144
+ self.close();
145
+ }
146
+ };
147
+ window.addEventListener('scroll', scrollHandler, { passive: true });
148
+ this._cleanupFunctions.push(() => window.removeEventListener('scroll', scrollHandler));
149
+ },
150
+
151
+ /**
152
+ * Bind triggers to all images with data-image-box attribute
153
+ */
154
+ bindTriggers: function () {
155
+ const self = this;
156
+ const triggers = document.querySelectorAll('[data-image-box]');
157
+
158
+ triggers.forEach(function (trigger) {
159
+ // Skip if already initialized
160
+ if (trigger.dataset.imageBoxInitialized) return;
161
+ trigger.dataset.imageBoxInitialized = 'true';
162
+
163
+ // Add trigger class
164
+ trigger.classList.add('vd-image-box-trigger');
165
+
166
+ // Handle broken images
167
+ if (trigger.tagName === 'IMG') {
168
+ // Check if already in error state
169
+ if (trigger.complete && trigger.naturalWidth === 0) {
170
+ trigger.classList.add('is-broken');
171
+ }
172
+
173
+ // Listen for error events
174
+ const errorHandler = function () {
175
+ trigger.classList.add('is-broken');
176
+ };
177
+ trigger.addEventListener('error', errorHandler);
178
+
179
+ // Listen for successful load
180
+ const loadHandler = function () {
181
+ trigger.classList.remove('is-broken');
182
+ };
183
+ trigger.addEventListener('load', loadHandler);
184
+ }
185
+
186
+ // Bind click event
187
+ const clickHandler = function (e) {
188
+ e.preventDefault();
189
+ self.open(trigger);
190
+ };
191
+ trigger.addEventListener('click', clickHandler);
192
+
193
+ // Store cleanup
194
+ trigger._imageBoxCleanup = () => trigger.removeEventListener('click', clickHandler);
195
+
196
+ // Keyboard accessibility for non-button triggers
197
+ if (trigger.tagName !== 'BUTTON' && trigger.tagName !== 'A') {
198
+ trigger.setAttribute('role', 'button');
199
+ trigger.setAttribute('tabindex', '0');
200
+ trigger.setAttribute('aria-label', 'View enlarged image');
201
+
202
+ const keyHandler = function (e) {
203
+ if (e.key === 'Enter' || e.key === ' ') {
204
+ e.preventDefault();
205
+ self.open(trigger);
206
+ }
207
+ };
208
+ trigger.addEventListener('keydown', keyHandler);
209
+
210
+ const originalCleanup = trigger._imageBoxCleanup;
211
+ trigger._imageBoxCleanup = () => {
212
+ originalCleanup();
213
+ trigger.removeEventListener('keydown', keyHandler);
214
+ };
215
+ }
216
+ });
217
+ },
218
+
219
+ /**
220
+ * Open image box
221
+ * @param {HTMLElement} trigger - The trigger element
222
+ */
223
+ open: function (trigger) {
224
+ if (this.isOpen) return;
225
+
226
+ this.currentTrigger = trigger;
227
+ this.isOpen = true;
228
+ this.initialScrollY = window.scrollY;
229
+
230
+ // Get image source - support dual images (thumbnail + full-size)
231
+ // data-image-box-full-src takes precedence for the lightbox
232
+ const imgSrc = trigger.dataset.imageBoxFullSrc ||
233
+ trigger.dataset.imageBoxSrc ||
234
+ trigger.src ||
235
+ trigger.href;
236
+
237
+ if (!imgSrc) {
238
+ console.warn('[Vanduo ImageBox] No image source found for trigger:', trigger);
239
+ return;
240
+ }
241
+
242
+ // Get caption
243
+ const captionText = trigger.dataset.imageBoxCaption || trigger.alt || '';
244
+
245
+ // Set image source
246
+ this.img.src = imgSrc;
247
+ this.img.alt = trigger.alt || '';
248
+
249
+ // Set caption
250
+ if (captionText) {
251
+ this.caption.textContent = captionText;
252
+ this.caption.style.display = 'block';
253
+ } else {
254
+ this.caption.style.display = 'none';
255
+ }
256
+
257
+ // Calculate scrollbar width and lock body scroll
258
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
259
+ document.body.style.setProperty('--scrollbar-width', `${scrollbarWidth}px`);
260
+ document.body.classList.add('body-image-box-open');
261
+
262
+ // Show backdrop
263
+ this.backdrop.classList.add('is-visible');
264
+
265
+ // Focus management
266
+ this.backdrop.focus();
267
+
268
+ // Dispatch event
269
+ trigger.dispatchEvent(new CustomEvent('imageBox:open', {
270
+ bubbles: true,
271
+ detail: { src: imgSrc }
272
+ }));
273
+
274
+ // Handle image load
275
+ if (!this.img.complete) {
276
+ this.img.style.opacity = '0';
277
+ this.img.onload = () => {
278
+ this.img.style.opacity = '';
279
+ };
280
+ }
281
+ },
282
+
283
+ /**
284
+ * Close image box
285
+ */
286
+ close: function () {
287
+ if (!this.isOpen) return;
288
+
289
+ this.isOpen = false;
290
+
291
+ // Hide backdrop
292
+ this.backdrop.classList.remove('is-visible');
293
+
294
+ // Unlock body scroll
295
+ document.body.classList.remove('body-image-box-open');
296
+ document.body.style.removeProperty('--scrollbar-width');
297
+
298
+ // Return focus to trigger
299
+ if (this.currentTrigger) {
300
+ this.currentTrigger.focus();
301
+ this.currentTrigger.dispatchEvent(new CustomEvent('imageBox:close', { bubbles: true }));
302
+ this.currentTrigger = null;
303
+ }
304
+
305
+ // Clear image after transition
306
+ setTimeout(() => {
307
+ if (!this.isOpen) {
308
+ this.img.src = '';
309
+ this.img.alt = '';
310
+ }
311
+ }, 300);
312
+ },
313
+
314
+ /**
315
+ * Reinitialize - useful after dynamic DOM changes
316
+ */
317
+ reinit: function () {
318
+ this.bindTriggers();
319
+ },
320
+
321
+ /**
322
+ * Destroy component and clean up
323
+ */
324
+ destroy: function () {
325
+ // Close if open
326
+ if (this.isOpen) {
327
+ this.close();
328
+ }
329
+
330
+ // Remove backdrop
331
+ if (this.backdrop && this.backdrop.parentNode) {
332
+ this.backdrop.parentNode.removeChild(this.backdrop);
333
+ }
334
+
335
+ // Run cleanup functions
336
+ this._cleanupFunctions.forEach(fn => fn());
337
+ this._cleanupFunctions = [];
338
+
339
+ // Remove trigger bindings
340
+ const triggers = document.querySelectorAll('[data-image-box-initialized]');
341
+ triggers.forEach(trigger => {
342
+ trigger.classList.remove('vd-image-box-trigger');
343
+ if (trigger._imageBoxCleanup) {
344
+ trigger._imageBoxCleanup();
345
+ delete trigger._imageBoxCleanup;
346
+ }
347
+ delete trigger.dataset.imageBoxInitialized;
348
+ });
349
+
350
+ this.backdrop = null;
351
+ this.container = null;
352
+ this.img = null;
353
+ this.closeBtn = null;
354
+ this.caption = null;
355
+ this.currentTrigger = null;
356
+ this.isOpen = false;
357
+ },
358
+
359
+ destroyAll: function () {
360
+ this.destroy();
361
+ }
362
+ };
363
+
364
+ // Register with Vanduo framework if available
365
+ if (typeof window.Vanduo !== 'undefined') {
366
+ window.Vanduo.register('imageBox', ImageBox);
367
+ }
368
+
369
+ // Expose globally
370
+ window.VanduoImageBox = ImageBox;
371
+
372
+ })();