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.
- package/LICENSE +35 -0
- package/README.md +205 -0
- package/css/components/alerts.css +224 -0
- package/css/components/avatar.css +275 -0
- package/css/components/badges.css +230 -0
- package/css/components/breadcrumbs.css +146 -0
- package/css/components/button-group.css +82 -0
- package/css/components/buttons.css +530 -0
- package/css/components/cards.css +304 -0
- package/css/components/chips.css +259 -0
- package/css/components/code-snippet.css +555 -0
- package/css/components/collapsible.css +267 -0
- package/css/components/collections.css +253 -0
- package/css/components/doc-search.css +464 -0
- package/css/components/doc-tabs.css +38 -0
- package/css/components/draggable.css +317 -0
- package/css/components/dropdown.css +266 -0
- package/css/components/footer.css +375 -0
- package/css/components/forms.css +1774 -0
- package/css/components/image-box.css +279 -0
- package/css/components/modals.css +285 -0
- package/css/components/navbar.css +530 -0
- package/css/components/pagination.css +186 -0
- package/css/components/preloader.css +340 -0
- package/css/components/progress.css +107 -0
- package/css/components/sidenav.css +301 -0
- package/css/components/skeleton.css +241 -0
- package/css/components/spinner.css +144 -0
- package/css/components/tabs.css +327 -0
- package/css/components/theme-customizer.css +835 -0
- package/css/components/toast.css +357 -0
- package/css/components/tooltips.css +270 -0
- package/css/core/colors.css +1017 -0
- package/css/core/fonts.css +266 -0
- package/css/core/grid.css +1699 -0
- package/css/core/helpers.css +2202 -0
- package/css/core/reset.css +128 -0
- package/css/core/tokens.css +213 -0
- package/css/core/typography.css +405 -0
- package/css/core/vd-aliases.css +47 -0
- package/css/effects/parallax.css +113 -0
- package/css/icons/icons-all.css +23 -0
- package/css/icons/icons.css +25 -0
- package/css/utilities/media.css +167 -0
- package/css/utilities/print.css +111 -0
- package/css/utilities/shadow.css +243 -0
- package/css/utilities/table.css +381 -0
- package/css/utilities/transforms.css +71 -0
- package/css/utilities/transitions.css +87 -0
- package/css/vanduo.css +80 -0
- package/dist/build-info.json +6 -0
- package/dist/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
- package/dist/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
- package/dist/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
- package/dist/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
- package/dist/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
- package/dist/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
- package/dist/fonts/inter/inter-bold.woff2 +0 -0
- package/dist/fonts/inter/inter-medium.woff2 +0 -0
- package/dist/fonts/inter/inter-regular.woff2 +0 -0
- package/dist/fonts/inter/inter-semibold.woff2 +0 -0
- package/dist/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
- package/dist/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
- package/dist/fonts/open-sans/open-sans-bold.woff2 +0 -0
- package/dist/fonts/open-sans/open-sans-medium.woff2 +0 -0
- package/dist/fonts/open-sans/open-sans-regular.woff2 +0 -0
- package/dist/fonts/rubik/rubik-bold.woff2 +0 -0
- package/dist/fonts/rubik/rubik-medium.woff2 +0 -0
- package/dist/fonts/rubik/rubik-regular.woff2 +0 -0
- package/dist/fonts/source-sans/source-sans-bold.woff2 +0 -0
- package/dist/fonts/source-sans/source-sans-regular.woff2 +0 -0
- package/dist/fonts/source-sans/source-sans-semibold.woff2 +0 -0
- package/dist/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
- package/dist/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
- package/dist/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
- package/dist/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
- package/dist/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
- package/dist/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
- package/dist/icons/phosphor/LICENSE +21 -0
- package/dist/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
- package/dist/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
- package/dist/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
- package/dist/icons/phosphor/bold/style.css +4627 -0
- package/dist/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
- package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
- package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
- package/dist/icons/phosphor/duotone/style.css +12115 -0
- package/dist/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
- package/dist/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
- package/dist/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
- package/dist/icons/phosphor/fill/style.css +4627 -0
- package/dist/icons/phosphor/light/Phosphor-Light.ttf +0 -0
- package/dist/icons/phosphor/light/Phosphor-Light.woff +0 -0
- package/dist/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
- package/dist/icons/phosphor/light/style.css +4627 -0
- package/dist/icons/phosphor/regular/Phosphor.ttf +0 -0
- package/dist/icons/phosphor/regular/Phosphor.woff +0 -0
- package/dist/icons/phosphor/regular/Phosphor.woff2 +0 -0
- package/dist/icons/phosphor/regular/style.css +4627 -0
- package/dist/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
- package/dist/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
- package/dist/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
- package/dist/icons/phosphor/thin/style.css +4627 -0
- package/dist/vanduo.cjs.js +5569 -0
- package/dist/vanduo.cjs.js.map +7 -0
- package/dist/vanduo.cjs.min.js +48 -0
- package/dist/vanduo.cjs.min.js.map +7 -0
- package/dist/vanduo.css +60666 -0
- package/dist/vanduo.css.map +1 -0
- package/dist/vanduo.esm.js +5548 -0
- package/dist/vanduo.esm.js.map +7 -0
- package/dist/vanduo.esm.min.js +48 -0
- package/dist/vanduo.esm.min.js.map +7 -0
- package/dist/vanduo.js +5545 -0
- package/dist/vanduo.js.map +7 -0
- package/dist/vanduo.min.css +2 -0
- package/dist/vanduo.min.css.map +1 -0
- package/dist/vanduo.min.js +48 -0
- package/dist/vanduo.min.js.map +7 -0
- package/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
- package/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
- package/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
- package/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
- package/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
- package/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
- package/fonts/inter/inter-bold.woff2 +0 -0
- package/fonts/inter/inter-medium.woff2 +0 -0
- package/fonts/inter/inter-regular.woff2 +0 -0
- package/fonts/inter/inter-semibold.woff2 +0 -0
- package/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
- package/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
- package/fonts/open-sans/open-sans-bold.woff2 +0 -0
- package/fonts/open-sans/open-sans-medium.woff2 +0 -0
- package/fonts/open-sans/open-sans-regular.woff2 +0 -0
- package/fonts/rubik/rubik-bold.woff2 +0 -0
- package/fonts/rubik/rubik-medium.woff2 +0 -0
- package/fonts/rubik/rubik-regular.woff2 +0 -0
- package/fonts/source-sans/source-sans-bold.woff2 +0 -0
- package/fonts/source-sans/source-sans-regular.woff2 +0 -0
- package/fonts/source-sans/source-sans-semibold.woff2 +0 -0
- package/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
- package/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
- package/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
- package/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
- package/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
- package/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
- package/icons/phosphor/LICENSE +21 -0
- package/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
- package/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
- package/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
- package/icons/phosphor/bold/style.css +4627 -0
- package/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
- package/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
- package/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
- package/icons/phosphor/duotone/style.css +12115 -0
- package/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
- package/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
- package/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
- package/icons/phosphor/fill/style.css +4627 -0
- package/icons/phosphor/light/Phosphor-Light.ttf +0 -0
- package/icons/phosphor/light/Phosphor-Light.woff +0 -0
- package/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
- package/icons/phosphor/light/style.css +4627 -0
- package/icons/phosphor/regular/Phosphor.ttf +0 -0
- package/icons/phosphor/regular/Phosphor.woff +0 -0
- package/icons/phosphor/regular/Phosphor.woff2 +0 -0
- package/icons/phosphor/regular/style.css +4627 -0
- package/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
- package/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
- package/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
- package/icons/phosphor/thin/style.css +4627 -0
- package/js/components/code-snippet.js +639 -0
- package/js/components/collapsible.js +226 -0
- package/js/components/doc-search.js +936 -0
- package/js/components/draggable.js +725 -0
- package/js/components/dropdown.js +362 -0
- package/js/components/font-switcher.js +253 -0
- package/js/components/grid.js +279 -0
- package/js/components/image-box.js +372 -0
- package/js/components/modals.js +367 -0
- package/js/components/navbar.js +264 -0
- package/js/components/pagination.js +286 -0
- package/js/components/parallax.js +216 -0
- package/js/components/preloader.js +183 -0
- package/js/components/select.js +444 -0
- package/js/components/sidenav.js +303 -0
- package/js/components/tabs.js +303 -0
- package/js/components/theme-customizer.js +784 -0
- package/js/components/theme-switcher.js +183 -0
- package/js/components/toast.js +343 -0
- package/js/components/tooltips.js +306 -0
- package/js/index.js +52 -0
- package/js/utils/helpers.js +306 -0
- package/js/utils/lifecycle.js +135 -0
- package/js/vanduo.js +120 -0
- 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
|
+
})();
|