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,183 @@
1
+ /**
2
+ * Vanduo Framework - Theme Switcher
3
+ * Handles light/dark/system theme toggling and persistence
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ const ThemeSwitcher = {
10
+ isInitialized: false,
11
+ _mediaQuery: null,
12
+ _onMediaChange: null,
13
+
14
+ init: function () {
15
+ this.STORAGE_KEY = 'vanduo-theme-preference';
16
+ this.state = {
17
+ preference: this.getPreference() // 'light', 'dark', or 'system'
18
+ };
19
+
20
+ if (this.isInitialized) {
21
+ this.applyTheme();
22
+ this.renderUI();
23
+ this.updateUI();
24
+ return;
25
+ }
26
+
27
+ this.isInitialized = true;
28
+
29
+ this.applyTheme();
30
+ this.listenForSystemChanges();
31
+ this.renderUI();
32
+
33
+ console.log('Vanduo Theme Switcher initialized');
34
+ },
35
+
36
+ getPreference: function () {
37
+ return this.getStorageValue(this.STORAGE_KEY, 'system');
38
+ },
39
+
40
+ setPreference: function (pref) {
41
+ this.state.preference = pref;
42
+ this.setStorageValue(this.STORAGE_KEY, pref);
43
+ this.applyTheme();
44
+ this.updateUI();
45
+ },
46
+
47
+ getStorageValue: function (key, fallback) {
48
+ if (typeof window.safeStorageGet === 'function') {
49
+ return window.safeStorageGet(key, fallback);
50
+ }
51
+ try {
52
+ const value = localStorage.getItem(key);
53
+ return value !== null ? value : fallback;
54
+ } catch (_e) {
55
+ return fallback;
56
+ }
57
+ },
58
+
59
+ setStorageValue: function (key, value) {
60
+ if (typeof window.safeStorageSet === 'function') {
61
+ return window.safeStorageSet(key, value);
62
+ }
63
+ try {
64
+ localStorage.setItem(key, value);
65
+ return true;
66
+ } catch (_e) {
67
+ return false;
68
+ }
69
+ },
70
+
71
+ applyTheme: function () {
72
+ const pref = this.state.preference;
73
+
74
+ if (pref === 'system') {
75
+ // When in system mode, we remove the data attribute to let the media query take over
76
+ // or we can explicitly set it. Explicitly setting it ensures consistency if we rely on [data-theme]
77
+ // But if we rely on @media in CSS, we might want to remove attributes.
78
+ // However, our CSS strategy uses :root:not([data-theme="light"]) inside media query for system dark fallback
79
+ // which is a bit complex.
80
+
81
+ // Simpler approach:
82
+ // If preference is system, REMOVE data-theme attribute. Let CSS media queries handle it.
83
+ document.documentElement.removeAttribute('data-theme');
84
+ } else {
85
+ document.documentElement.setAttribute('data-theme', pref);
86
+ }
87
+ },
88
+
89
+ listenForSystemChanges: function () {
90
+ if (this._mediaQuery && this._onMediaChange) {
91
+ return;
92
+ }
93
+
94
+ this._mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
95
+ this._onMediaChange = _e => {
96
+ if (this.state.preference === 'system') {
97
+ // Re-apply (effectively just to ensure consistency, though removing attribute usually suffices)
98
+ this.applyTheme();
99
+ }
100
+ };
101
+ this._mediaQuery.addEventListener('change', this._onMediaChange);
102
+ },
103
+
104
+ // Helper to facilitate UI creation if needed, though often UI is in HTML
105
+ renderUI: function () {
106
+ // Look for any uninitialized theme toggles
107
+ const toggles = document.querySelectorAll('[data-toggle="theme"]');
108
+ toggles.forEach(toggle => {
109
+ if (toggle.getAttribute('data-theme-initialized') === 'true') {
110
+ if (toggle.tagName === 'SELECT') {
111
+ toggle.value = this.state.preference;
112
+ }
113
+ return;
114
+ }
115
+
116
+ // Simplified UI Binding - assumes a select or a button cycle
117
+ if (toggle.tagName === 'SELECT') {
118
+ toggle.value = this.state.preference;
119
+ const onChange = (e) => {
120
+ this.setPreference(e.target.value);
121
+ };
122
+ toggle.addEventListener('change', onChange);
123
+ toggle._themeToggleHandler = onChange;
124
+ } else {
125
+ // Button implementation (cycle)
126
+ const onClick = () => {
127
+ const modes = ['system', 'light', 'dark'];
128
+ const nextIndex = (modes.indexOf(this.state.preference) + 1) % modes.length;
129
+ this.setPreference(modes[nextIndex]);
130
+ };
131
+ toggle.addEventListener('click', onClick);
132
+ toggle._themeToggleHandler = onClick;
133
+ }
134
+ // Mark as initialized
135
+ toggle.setAttribute('data-theme-initialized', 'true');
136
+ });
137
+ },
138
+
139
+ updateUI: function () {
140
+ const toggles = document.querySelectorAll('[data-toggle="theme"]');
141
+ toggles.forEach(toggle => {
142
+ if (toggle.tagName === 'SELECT') {
143
+ toggle.value = this.state.preference;
144
+ } else {
145
+ // Update button text/icon if needed
146
+ // e.g. toggle.textContent = this.state.preference;
147
+ // For now, assume the user handles visual state or generic text
148
+
149
+ // If there is an icon or text span inside, update it
150
+ const span = toggle.querySelector('.theme-current-label');
151
+ if (span) {
152
+ span.textContent = this.state.preference.charAt(0).toUpperCase() + this.state.preference.slice(1);
153
+ }
154
+ }
155
+ });
156
+ },
157
+
158
+ destroyAll: function () {
159
+ const toggles = document.querySelectorAll('[data-toggle="theme"][data-theme-initialized="true"]');
160
+ toggles.forEach(toggle => {
161
+ if (toggle._themeToggleHandler) {
162
+ const eventName = toggle.tagName === 'SELECT' ? 'change' : 'click';
163
+ toggle.removeEventListener(eventName, toggle._themeToggleHandler);
164
+ delete toggle._themeToggleHandler;
165
+ }
166
+ toggle.removeAttribute('data-theme-initialized');
167
+ });
168
+
169
+ if (this._mediaQuery && this._onMediaChange) {
170
+ this._mediaQuery.removeEventListener('change', this._onMediaChange);
171
+ }
172
+
173
+ this._mediaQuery = null;
174
+ this._onMediaChange = null;
175
+ this.isInitialized = false;
176
+ }
177
+ };
178
+
179
+ // Register component
180
+ if (window.Vanduo) {
181
+ window.Vanduo.register('themeSwitcher', ThemeSwitcher);
182
+ }
183
+ })();
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Vanduo Framework - Toast Component
3
+ * Popup notifications for user feedback
4
+ */
5
+
6
+ (function() {
7
+ 'use strict';
8
+
9
+ /**
10
+ * Toast Component
11
+ */
12
+ const Toast = {
13
+ // Default options
14
+ defaults: {
15
+ position: 'top-right',
16
+ duration: 5000,
17
+ dismissible: true,
18
+ showProgress: true,
19
+ pauseOnHover: true
20
+ },
21
+
22
+ // Container cache
23
+ containers: {},
24
+
25
+ /**
26
+ * Get or create a toast container for a position
27
+ * @param {string} position - Container position
28
+ * @returns {HTMLElement} Toast container element
29
+ */
30
+ getContainer: function(position) {
31
+ if (this.containers[position]) {
32
+ return this.containers[position];
33
+ }
34
+
35
+ const container = document.createElement('div');
36
+ container.className = `vd-toast-container vd-toast-container-${position}`;
37
+ container.setAttribute('role', 'status');
38
+ container.setAttribute('aria-live', 'polite');
39
+ container.setAttribute('aria-atomic', 'false');
40
+ document.body.appendChild(container);
41
+ this.containers[position] = container;
42
+
43
+ return container;
44
+ },
45
+
46
+ /**
47
+ * Show a toast notification
48
+ * @param {Object|string} options - Toast options or message string
49
+ * @param {string} [type] - Toast type (success, error, warning, info)
50
+ * @param {number} [duration] - Auto-dismiss duration in ms
51
+ * @returns {HTMLElement} Toast element
52
+ */
53
+ show: function(options, type, duration) {
54
+ // Support simple API: Toast.show('Message', 'success', 3000)
55
+ if (typeof options === 'string') {
56
+ options = {
57
+ message: options,
58
+ type: type,
59
+ duration: duration
60
+ };
61
+ }
62
+
63
+ const config = Object.assign({}, this.defaults, options);
64
+ const container = this.getContainer(config.position);
65
+
66
+ // Create toast element
67
+ const toast = document.createElement('div');
68
+ toast.className = 'vd-toast';
69
+
70
+ if (config.type) {
71
+ toast.classList.add(`vd-toast-${config.type}`);
72
+ }
73
+
74
+ if (config.solid) {
75
+ toast.classList.add('vd-toast-solid');
76
+ }
77
+
78
+ if (config.showProgress && config.duration > 0) {
79
+ toast.classList.add('vd-toast-with-progress');
80
+ }
81
+
82
+ // Build toast content
83
+ let html = '';
84
+
85
+ // Icon (sanitize custom icons, default icons are trusted SVG)
86
+ if (config.icon) {
87
+ const safeIcon = typeof sanitizeHtml === 'function' ? sanitizeHtml(config.icon) : escapeHtml(config.icon);
88
+ html += `<span class="vd-toast-icon">${safeIcon}</span>`;
89
+ } else if (config.type) {
90
+ html += `<span class="vd-toast-icon">${this.getDefaultIcon(config.type)}</span>`;
91
+ }
92
+
93
+ // Local escape helper — guarantees HTML-safe output even if the
94
+ // global escapeHtml utility is not loaded in the current bundle.
95
+ const _esc = typeof escapeHtml === 'function'
96
+ ? escapeHtml
97
+ : function (s) {
98
+ const d = document.createElement('div');
99
+ d.appendChild(document.createTextNode(s));
100
+ return d.innerHTML;
101
+ };
102
+
103
+ // Content (escape text to prevent injection)
104
+ html += '<div class="vd-toast-content">';
105
+ if (config.title) {
106
+ html += `<div class="vd-toast-title">${_esc(String(config.title))}</div>`;
107
+ }
108
+ if (config.message) {
109
+ html += `<div class="vd-toast-message">${_esc(String(config.message))}</div>`;
110
+ }
111
+ html += '</div>';
112
+
113
+ // Close button
114
+ if (config.dismissible) {
115
+ html += '<button type="button" class="vd-toast-close" aria-label="Close"></button>';
116
+ }
117
+
118
+ // Progress bar
119
+ if (config.showProgress && config.duration > 0) {
120
+ const safeDuration = parseInt(config.duration, 10) || 0;
121
+ html += `<div class="vd-toast-progress" style="animation-duration: ${safeDuration}ms"></div>`;
122
+ }
123
+
124
+ toast.innerHTML = html;
125
+
126
+ // Add to container
127
+ container.appendChild(toast);
128
+
129
+ toast._toastCleanup = [];
130
+
131
+ // Set up close button handler
132
+ if (config.dismissible) {
133
+ const closeBtn = toast.querySelector('.vd-toast-close');
134
+ const onClose = () => {
135
+ this.dismiss(toast);
136
+ };
137
+ closeBtn.addEventListener('click', onClose);
138
+ toast._toastCleanup.push(() => closeBtn.removeEventListener('click', onClose));
139
+ }
140
+
141
+ // Pause on hover
142
+ let timeoutId = null;
143
+ let remainingTime = config.duration;
144
+ let startTime = null;
145
+
146
+ const startTimer = () => {
147
+ if (config.duration > 0) {
148
+ startTime = Date.now();
149
+ timeoutId = setTimeout(() => {
150
+ this.dismiss(toast);
151
+ }, remainingTime);
152
+ toast._toastTimeoutId = timeoutId;
153
+
154
+ // Resume progress animation
155
+ const progress = toast.querySelector('.vd-toast-progress');
156
+ if (progress) {
157
+ progress.style.animationPlayState = 'running';
158
+ }
159
+ }
160
+ };
161
+
162
+ const pauseTimer = () => {
163
+ if (timeoutId) {
164
+ clearTimeout(timeoutId);
165
+ timeoutId = null;
166
+ toast._toastTimeoutId = null;
167
+ remainingTime -= Date.now() - startTime;
168
+
169
+ // Pause progress animation
170
+ const progress = toast.querySelector('.vd-toast-progress');
171
+ if (progress) {
172
+ progress.style.animationPlayState = 'paused';
173
+ }
174
+ }
175
+ };
176
+
177
+ if (config.pauseOnHover) {
178
+ toast.addEventListener('mouseenter', pauseTimer);
179
+ toast.addEventListener('mouseleave', startTimer);
180
+ toast._toastCleanup.push(
181
+ () => toast.removeEventListener('mouseenter', pauseTimer),
182
+ () => toast.removeEventListener('mouseleave', startTimer)
183
+ );
184
+ }
185
+
186
+ // Trigger enter animation
187
+ requestAnimationFrame(() => {
188
+ toast.classList.add('is-visible');
189
+ startTimer();
190
+ });
191
+
192
+ // Store config on element for later access
193
+ toast._toastConfig = config;
194
+
195
+ // Dispatch show event
196
+ const showEvent = new CustomEvent('toast:show', {
197
+ bubbles: true,
198
+ detail: { toast, config }
199
+ });
200
+ toast.dispatchEvent(showEvent);
201
+
202
+ return toast;
203
+ },
204
+
205
+ /**
206
+ * Dismiss a toast
207
+ * @param {HTMLElement} toast - Toast element to dismiss
208
+ */
209
+ dismiss: function(toast) {
210
+ if (!toast || toast.classList.contains('is-exiting')) return;
211
+
212
+ if (toast._toastTimeoutId) {
213
+ clearTimeout(toast._toastTimeoutId);
214
+ toast._toastTimeoutId = null;
215
+ }
216
+
217
+ toast.classList.remove('is-visible');
218
+ toast.classList.add('is-exiting');
219
+
220
+ // Dispatch dismiss event
221
+ const dismissEvent = new CustomEvent('toast:dismiss', {
222
+ bubbles: true,
223
+ detail: { toast }
224
+ });
225
+ toast.dispatchEvent(dismissEvent);
226
+
227
+ // Remove after animation
228
+ const handleTransitionEnd = () => {
229
+ toast.removeEventListener('transitionend', handleTransitionEnd);
230
+ if (toast._toastCleanup) {
231
+ toast._toastCleanup.forEach(fn => fn());
232
+ delete toast._toastCleanup;
233
+ }
234
+ if (toast.parentElement) {
235
+ toast.parentElement.removeChild(toast);
236
+ }
237
+ };
238
+
239
+ toast.addEventListener('transitionend', handleTransitionEnd);
240
+
241
+ // Fallback removal if transition doesn't fire
242
+ setTimeout(() => {
243
+ if (toast._toastCleanup) {
244
+ toast._toastCleanup.forEach(fn => fn());
245
+ delete toast._toastCleanup;
246
+ }
247
+ if (toast.parentElement) {
248
+ toast.parentElement.removeChild(toast);
249
+ }
250
+ }, 400);
251
+ },
252
+
253
+ /**
254
+ * Destroy all toasts and containers
255
+ */
256
+ destroyAll: function() {
257
+ Object.keys(this.containers).forEach(position => {
258
+ const container = this.containers[position];
259
+ if (!container) return;
260
+
261
+ const toasts = container.querySelectorAll('.vd-toast');
262
+ toasts.forEach(toast => {
263
+ if (toast._toastTimeoutId) {
264
+ clearTimeout(toast._toastTimeoutId);
265
+ }
266
+ if (toast._toastCleanup) {
267
+ toast._toastCleanup.forEach(fn => fn());
268
+ delete toast._toastCleanup;
269
+ }
270
+ if (toast.parentElement) {
271
+ toast.parentElement.removeChild(toast);
272
+ }
273
+ });
274
+
275
+ if (container.parentElement) {
276
+ container.parentElement.removeChild(container);
277
+ }
278
+ });
279
+
280
+ this.containers = {};
281
+ },
282
+
283
+ /**
284
+ * Dismiss all toasts
285
+ * @param {string} [position] - Optional position to clear (clears all if not specified)
286
+ */
287
+ dismissAll: function(position) {
288
+ if (position && this.containers[position]) {
289
+ const toasts = this.containers[position].querySelectorAll('.vd-toast');
290
+ toasts.forEach(toast => this.dismiss(toast));
291
+ } else {
292
+ Object.values(this.containers).forEach(container => {
293
+ const toasts = container.querySelectorAll('.vd-toast');
294
+ toasts.forEach(toast => this.dismiss(toast));
295
+ });
296
+ }
297
+ },
298
+
299
+ /**
300
+ * Get default icon SVG for a type
301
+ * @param {string} type - Toast type
302
+ * @returns {string} SVG icon markup
303
+ */
304
+ getDefaultIcon: function(type) {
305
+ const icons = {
306
+ success: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>',
307
+ error: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>',
308
+ warning: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>',
309
+ info: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>'
310
+ };
311
+
312
+ return icons[type] || '';
313
+ },
314
+
315
+ /**
316
+ * Convenience methods for common toast types
317
+ */
318
+ success: function(message, options) {
319
+ return this.show(Object.assign({ message, type: 'success' }, options));
320
+ },
321
+
322
+ error: function(message, options) {
323
+ return this.show(Object.assign({ message, type: 'error' }, options));
324
+ },
325
+
326
+ warning: function(message, options) {
327
+ return this.show(Object.assign({ message, type: 'warning' }, options));
328
+ },
329
+
330
+ info: function(message, options) {
331
+ return this.show(Object.assign({ message, type: 'info' }, options));
332
+ }
333
+ };
334
+
335
+ // Register with Vanduo framework if available
336
+ if (typeof window.Vanduo !== 'undefined') {
337
+ window.Vanduo.register('toast', Toast);
338
+ }
339
+
340
+ // Also expose globally for convenience
341
+ window.Toast = Toast;
342
+
343
+ })();