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,784 @@
1
+ /**
2
+ * Vanduo Framework - Theme Customizer
3
+ * A comprehensive theme customization component for the navbar
4
+ * Handles primary color, neutral color, radius, font, and color mode
5
+ */
6
+
7
+ (function () {
8
+ 'use strict';
9
+
10
+ const ThemeCustomizer = {
11
+ // Storage keys
12
+ STORAGE_KEYS: {
13
+ PRIMARY: 'vanduo-primary-color',
14
+ NEUTRAL: 'vanduo-neutral-color',
15
+ RADIUS: 'vanduo-radius',
16
+ FONT: 'vanduo-font-preference',
17
+ THEME: 'vanduo-theme-preference'
18
+ },
19
+
20
+ // Default values
21
+ DEFAULTS: {
22
+ PRIMARY_LIGHT: 'black',
23
+ PRIMARY_DARK: 'amber',
24
+ NEUTRAL: 'neutral',
25
+ RADIUS: '0.5',
26
+ FONT: 'ubuntu',
27
+ THEME: 'system'
28
+ },
29
+
30
+ // Primary color definitions (Open Color based)
31
+ PRIMARY_COLORS: {
32
+ 'black': { name: 'Black', color: '#000000' },
33
+ 'red': { name: 'Red', color: '#fa5252' },
34
+ 'orange': { name: 'Orange', color: '#fd7e14' },
35
+ 'amber': { name: 'Amber', color: '#f59f00' },
36
+ 'yellow': { name: 'Yellow', color: '#fcc419' },
37
+ 'lime': { name: 'Lime', color: '#82c91e' },
38
+ 'green': { name: 'Green', color: '#40c057' },
39
+ 'emerald': { name: 'Emerald', color: '#20c997' },
40
+ 'teal': { name: 'Teal', color: '#12b886' },
41
+ 'cyan': { name: 'Cyan', color: '#22b8cf' },
42
+ 'sky': { name: 'Sky', color: '#3bc9db' },
43
+ 'blue': { name: 'Blue', color: '#228be6' },
44
+ 'indigo': { name: 'Indigo', color: '#4c6ef5' },
45
+ 'violet': { name: 'Violet', color: '#7950f2' },
46
+ 'purple': { name: 'Purple', color: '#be4bdb' },
47
+ 'fuchsia': { name: 'Fuchsia', color: '#f06595' },
48
+ 'pink': { name: 'Pink', color: '#e64980' },
49
+ 'rose': { name: 'Rose', color: '#ff8787' }
50
+ },
51
+
52
+ // Neutral color definitions
53
+ NEUTRAL_COLORS: {
54
+ 'slate': { name: 'Slate', color: '#64748b' },
55
+ 'gray': { name: 'Gray', color: '#6b7280' },
56
+ 'zinc': { name: 'Zinc', color: '#71717a' },
57
+ 'neutral': { name: 'Neutral', color: '#737373' },
58
+ 'stone': { name: 'Stone', color: '#78716c' }
59
+ },
60
+
61
+ // Radius options
62
+ RADIUS_OPTIONS: ['0', '0.125', '0.25', '0.375', '0.5'],
63
+
64
+ // Font options
65
+ FONT_OPTIONS: {
66
+ 'jetbrains-mono': { name: 'JetBrains Mono', family: "'JetBrains Mono', monospace" },
67
+ 'inter': { name: 'Inter', family: "'Inter', sans-serif" },
68
+ 'source-sans': { name: 'Source Sans 3', family: "'Source Sans 3', sans-serif" },
69
+ 'fira-sans': { name: 'Fira Sans', family: "'Fira Sans', sans-serif" },
70
+ 'ibm-plex': { name: 'IBM Plex Sans', family: "'IBM Plex Sans', sans-serif" },
71
+ 'system': { name: 'System Default', family: null },
72
+ // Google Fonts Collection
73
+ 'ubuntu': { name: 'Ubuntu', family: "'Ubuntu', sans-serif" },
74
+ 'open-sans': { name: 'Open Sans', family: "'Open Sans', sans-serif" },
75
+ 'rubik': { name: 'Rubik', family: "'Rubik', sans-serif" },
76
+ 'titillium-web': { name: 'Titillium Web', family: "'Titillium Web', sans-serif" }
77
+ },
78
+
79
+ // Theme mode options
80
+ THEME_MODES: ['system', 'dark', 'light'],
81
+
82
+ // State
83
+ state: {
84
+ primary: null,
85
+ neutral: null,
86
+ radius: null,
87
+ font: null,
88
+ theme: null,
89
+ isOpen: false
90
+ },
91
+
92
+ isInitialized: false,
93
+ _cleanup: [],
94
+
95
+ // DOM references
96
+ elements: {
97
+ customizer: null,
98
+ trigger: null,
99
+ panel: null,
100
+ overlay: null
101
+ },
102
+
103
+ /**
104
+ * Initialize the Theme Customizer
105
+ */
106
+ init: function () {
107
+ if (this.isInitialized) {
108
+ this.bindExistingElements();
109
+ this.bindPanelEvents();
110
+ this.updateUI();
111
+ return;
112
+ }
113
+
114
+ this.isInitialized = true;
115
+ this._cleanup = [];
116
+
117
+ this.loadPreferences();
118
+ this.applyAllPreferences();
119
+ this.bindExistingElements();
120
+ this.bindEvents();
121
+
122
+ console.log('Vanduo Theme Customizer initialized');
123
+ },
124
+
125
+ addListener: function (target, event, handler, options) {
126
+ if (!target) return;
127
+ target.addEventListener(event, handler, options);
128
+ this._cleanup.push(() => target.removeEventListener(event, handler, options));
129
+ },
130
+
131
+ /**
132
+ * Get default primary color based on theme
133
+ */
134
+ getDefaultPrimary: function (theme) {
135
+ if (theme === 'system') {
136
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
137
+ return this.DEFAULTS.PRIMARY_DARK;
138
+ }
139
+ return this.DEFAULTS.PRIMARY_LIGHT;
140
+ }
141
+ return theme === 'dark' ? this.DEFAULTS.PRIMARY_DARK : this.DEFAULTS.PRIMARY_LIGHT;
142
+ },
143
+
144
+ /**
145
+ * Load preferences from localStorage
146
+ */
147
+ loadPreferences: function () {
148
+ this.state.theme = this.getStorageValue(this.STORAGE_KEYS.THEME, this.DEFAULTS.THEME);
149
+ this.state.primary = this.getStorageValue(this.STORAGE_KEYS.PRIMARY, this.getDefaultPrimary(this.state.theme));
150
+ this.state.neutral = this.getStorageValue(this.STORAGE_KEYS.NEUTRAL, this.DEFAULTS.NEUTRAL);
151
+ this.state.radius = this.getStorageValue(this.STORAGE_KEYS.RADIUS, this.DEFAULTS.RADIUS);
152
+ this.state.font = this.getStorageValue(this.STORAGE_KEYS.FONT, this.DEFAULTS.FONT);
153
+ },
154
+
155
+ /**
156
+ * Save a preference to localStorage
157
+ */
158
+ savePreference: function (key, value) {
159
+ this.setStorageValue(key, value);
160
+ },
161
+
162
+ /**
163
+ * Apply all preferences
164
+ */
165
+ applyAllPreferences: function () {
166
+ this.applyPrimary(this.state.primary);
167
+ this.applyNeutral(this.state.neutral);
168
+ this.applyRadius(this.state.radius);
169
+ this.applyFont(this.state.font);
170
+ this.applyTheme(this.state.theme);
171
+ },
172
+
173
+ /**
174
+ * Apply primary color
175
+ */
176
+ applyPrimary: function (colorKey) {
177
+ if (!this.PRIMARY_COLORS[colorKey]) {
178
+ colorKey = this.getDefaultPrimary(this.state.theme);
179
+ }
180
+
181
+ this.state.primary = colorKey;
182
+ document.documentElement.setAttribute('data-primary', colorKey);
183
+ this.savePreference(this.STORAGE_KEYS.PRIMARY, colorKey);
184
+
185
+ this.dispatchEvent('primary-change', { color: colorKey });
186
+ },
187
+
188
+ /**
189
+ * Apply neutral color
190
+ */
191
+ applyNeutral: function (neutralKey) {
192
+ if (!this.NEUTRAL_COLORS[neutralKey]) {
193
+ neutralKey = this.DEFAULTS.NEUTRAL;
194
+ }
195
+
196
+ this.state.neutral = neutralKey;
197
+ document.documentElement.setAttribute('data-neutral', neutralKey);
198
+ this.savePreference(this.STORAGE_KEYS.NEUTRAL, neutralKey);
199
+
200
+ this.dispatchEvent('neutral-change', { neutral: neutralKey });
201
+ },
202
+
203
+ /**
204
+ * Apply border radius
205
+ */
206
+ applyRadius: function (radius) {
207
+ if (!this.RADIUS_OPTIONS.includes(radius)) {
208
+ radius = this.DEFAULTS.RADIUS;
209
+ }
210
+
211
+ this.state.radius = radius;
212
+ document.documentElement.setAttribute('data-radius', radius);
213
+ document.documentElement.style.setProperty('--radius-scale', radius);
214
+ this.savePreference(this.STORAGE_KEYS.RADIUS, radius);
215
+
216
+ this.dispatchEvent('radius-change', { radius: radius });
217
+ },
218
+
219
+ /**
220
+ * Apply font family
221
+ */
222
+ applyFont: function (fontKey) {
223
+ if (!this.FONT_OPTIONS[fontKey]) {
224
+ fontKey = this.DEFAULTS.FONT;
225
+ }
226
+
227
+ this.state.font = fontKey;
228
+
229
+ if (fontKey === 'system') {
230
+ document.documentElement.removeAttribute('data-font');
231
+ } else {
232
+ document.documentElement.setAttribute('data-font', fontKey);
233
+ }
234
+
235
+ this.savePreference(this.STORAGE_KEYS.FONT, fontKey);
236
+
237
+ // Also update the existing FontSwitcher if available
238
+ if (window.FontSwitcher && window.FontSwitcher.setPreference) {
239
+ window.FontSwitcher.state.preference = fontKey;
240
+ window.FontSwitcher.applyFont();
241
+ }
242
+
243
+ this.dispatchEvent('font-change', { font: fontKey });
244
+ },
245
+
246
+ /**
247
+ * Apply theme mode
248
+ */
249
+ applyTheme: function (mode) {
250
+ if (!this.THEME_MODES.includes(mode)) {
251
+ mode = this.DEFAULTS.THEME;
252
+ }
253
+
254
+ // Check if we should switch primary color (if using default)
255
+ const oldDefault = this.getDefaultPrimary(this.state.theme);
256
+ if (this.state.primary === oldDefault) {
257
+ const newDefault = this.getDefaultPrimary(mode);
258
+ if (newDefault !== this.state.primary) {
259
+ this.applyPrimary(newDefault);
260
+ }
261
+ }
262
+
263
+ this.state.theme = mode;
264
+
265
+ if (mode === 'system') {
266
+ document.documentElement.removeAttribute('data-theme');
267
+ } else {
268
+ document.documentElement.setAttribute('data-theme', mode);
269
+ }
270
+
271
+ this.savePreference(this.STORAGE_KEYS.THEME, mode);
272
+
273
+ // Also update the existing ThemeSwitcher if available
274
+ if (window.Vanduo && window.Vanduo.components.themeSwitcher) {
275
+ const themeSwitcher = window.Vanduo.components.themeSwitcher;
276
+ if (themeSwitcher.state) {
277
+ themeSwitcher.state.preference = mode;
278
+ }
279
+ }
280
+
281
+ this.dispatchEvent('mode-change', { mode: mode });
282
+ },
283
+
284
+ /**
285
+ * Dispatch custom event
286
+ */
287
+ dispatchEvent: function (type, detail) {
288
+ const event = new CustomEvent('theme:' + type, {
289
+ bubbles: true,
290
+ detail: detail
291
+ });
292
+ document.dispatchEvent(event);
293
+
294
+ // Also dispatch a general change event
295
+ const changeEvent = new CustomEvent('theme:change', {
296
+ bubbles: true,
297
+ detail: {
298
+ type: type,
299
+ value: detail[Object.keys(detail)[0]],
300
+ state: { ...this.state }
301
+ }
302
+ });
303
+ document.dispatchEvent(changeEvent);
304
+ },
305
+
306
+ /**
307
+ * Bind to existing DOM elements or create them dynamically
308
+ */
309
+ bindExistingElements: function () {
310
+ // First check for existing full structure
311
+ this.elements.customizer = document.querySelector('.vd-theme-customizer');
312
+
313
+ if (this.elements.customizer) {
314
+ this.elements.trigger = this.elements.customizer.querySelector('.vd-theme-customizer-trigger');
315
+ this.elements.panel = this.elements.customizer.querySelector('.vd-theme-customizer-panel');
316
+ this.elements.overlay = this.elements.customizer.querySelector('.vd-theme-customizer-overlay');
317
+ } else {
318
+ // Look for standalone trigger button with data attribute
319
+ const standaloneTrigger = document.querySelector('[data-theme-customizer-trigger]');
320
+ if (standaloneTrigger) {
321
+ this.createDynamicPanel(standaloneTrigger);
322
+ }
323
+ }
324
+
325
+ // Update UI to reflect current state
326
+ this.updateUI();
327
+ },
328
+
329
+ /**
330
+ * Create the panel dynamically when only a trigger button exists
331
+ */
332
+ createDynamicPanel: function (triggerButton) {
333
+ // Create wrapper
334
+ const wrapper = document.createElement('div');
335
+ wrapper.className = 'vd-theme-customizer';
336
+
337
+ // Move trigger into wrapper or create reference
338
+ this.elements.trigger = triggerButton;
339
+
340
+ // Create overlay
341
+ const overlay = document.createElement('div');
342
+ overlay.className = 'vd-theme-customizer-overlay';
343
+
344
+ // Create panel
345
+ const panel = document.createElement('div');
346
+ panel.className = 'vd-theme-customizer-panel';
347
+ panel.innerHTML = this.getPanelHTML();
348
+
349
+ // Append to body
350
+ document.body.appendChild(overlay);
351
+ document.body.appendChild(panel);
352
+
353
+ // Store references
354
+ this.elements.panel = panel;
355
+ this.elements.overlay = overlay;
356
+ this.elements.customizer = { contains: (el) => panel.contains(el) || triggerButton.contains(el) };
357
+
358
+ // Position panel below trigger on desktop
359
+ this.positionPanel();
360
+
361
+ // Bind panel events after creation
362
+ this.bindPanelEvents();
363
+
364
+ // Reposition on resize
365
+ this.addListener(window, 'resize', () => this.positionPanel());
366
+ },
367
+
368
+ /**
369
+ * Position the panel below the trigger button on desktop
370
+ */
371
+ positionPanel: function () {
372
+ if (!this.elements.panel || !this.elements.trigger) return;
373
+
374
+ const isMobile = window.innerWidth < 768;
375
+
376
+ if (isMobile) {
377
+ // Mobile: full height slide-in from right - let CSS handle it
378
+ this.elements.panel.style.top = '';
379
+ this.elements.panel.style.right = '';
380
+ this.elements.panel.style.left = '';
381
+ this.elements.panel.style.height = '';
382
+ this.elements.panel.style.maxHeight = '';
383
+ } else {
384
+ // Desktop: position directly below the trigger button, aligned to its right edge
385
+ const triggerRect = this.elements.trigger.getBoundingClientRect();
386
+ const panelWidth = 320; // --customizer-width
387
+ const panelTop = triggerRect.bottom + 8;
388
+
389
+ // Calculate right position: align panel's right edge with trigger's right edge
390
+ // But ensure it doesn't overflow the viewport
391
+ const viewportWidth = window.innerWidth;
392
+ let panelRight = viewportWidth - triggerRect.right;
393
+
394
+ // Ensure panel doesn't overflow left side of viewport
395
+ const panelLeft = viewportWidth - panelRight - panelWidth;
396
+ if (panelLeft < 8) {
397
+ panelRight = viewportWidth - panelWidth - 8;
398
+ }
399
+
400
+ this.elements.panel.style.top = panelTop + 'px';
401
+ this.elements.panel.style.right = panelRight + 'px';
402
+ this.elements.panel.style.left = '';
403
+ this.elements.panel.style.height = 'auto';
404
+ this.elements.panel.style.maxHeight = 'calc(100vh - ' + panelTop + 'px)';
405
+ }
406
+ },
407
+
408
+ /**
409
+ * Bind events specifically for the panel (called after dynamic creation)
410
+ */
411
+ bindPanelEvents: function () {
412
+ if (!this.elements.panel) return;
413
+ if (this.elements.panel.getAttribute('data-customizer-initialized') === 'true') return;
414
+
415
+ this.elements.panel.setAttribute('data-customizer-initialized', 'true');
416
+
417
+ // Primary color swatches
418
+ this.elements.panel.querySelectorAll('[data-color]').forEach(swatch => {
419
+ this.addListener(swatch, 'click', () => {
420
+ this.applyPrimary(swatch.dataset.color);
421
+ this.updateUI();
422
+ });
423
+ });
424
+
425
+ // Neutral color swatches
426
+ this.elements.panel.querySelectorAll('[data-neutral]').forEach(swatch => {
427
+ this.addListener(swatch, 'click', () => {
428
+ this.applyNeutral(swatch.dataset.neutral);
429
+ this.updateUI();
430
+ });
431
+ });
432
+
433
+ // Radius buttons
434
+ this.elements.panel.querySelectorAll('[data-radius]').forEach(btn => {
435
+ this.addListener(btn, 'click', () => {
436
+ this.applyRadius(btn.dataset.radius);
437
+ this.updateUI();
438
+ });
439
+ });
440
+
441
+ // Font selector
442
+ const fontSelect = this.elements.panel.querySelector('[data-customizer-font]');
443
+ if (fontSelect) {
444
+ this.addListener(fontSelect, 'change', (e) => {
445
+ this.applyFont(e.target.value);
446
+ this.updateUI();
447
+ });
448
+ }
449
+
450
+ // Mode buttons
451
+ this.elements.panel.querySelectorAll('[data-mode]').forEach(btn => {
452
+ this.addListener(btn, 'click', () => {
453
+ this.applyTheme(btn.dataset.mode);
454
+ this.updateUI();
455
+ });
456
+ });
457
+
458
+ // Reset button
459
+ const resetBtn = this.elements.panel.querySelector('.customizer-reset');
460
+ if (resetBtn) {
461
+ this.addListener(resetBtn, 'click', () => {
462
+ this.reset();
463
+ });
464
+ }
465
+
466
+ // Mobile close button
467
+ const closeBtn = this.elements.panel.querySelector('.customizer-mobile-close');
468
+ if (closeBtn) {
469
+ this.addListener(closeBtn, 'click', () => {
470
+ this.close();
471
+ });
472
+ }
473
+
474
+ // Overlay click
475
+ if (this.elements.overlay) {
476
+ this.addListener(this.elements.overlay, 'click', () => {
477
+ this.close();
478
+ });
479
+ }
480
+ },
481
+
482
+ /**
483
+ * Generate panel HTML
484
+ */
485
+ getPanelHTML: function () {
486
+ // Generate primary color swatches
487
+ let primarySwatches = '';
488
+ for (const [key, value] of Object.entries(this.PRIMARY_COLORS)) {
489
+ primarySwatches += `<button class="tc-color-swatch${key === this.state.primary ? ' is-active' : ''}" data-color="${key}" style="--swatch-color: ${value.color}" title="${value.name}"></button>`;
490
+ }
491
+
492
+ // Generate neutral color swatches
493
+ let neutralSwatches = '';
494
+ for (const [key, value] of Object.entries(this.NEUTRAL_COLORS)) {
495
+ neutralSwatches += `<button class="tc-neutral-swatch${key === this.state.neutral ? ' is-active' : ''}" data-neutral="${key}" style="--swatch-color: ${value.color}" title="${value.name}"><span>${value.name}</span></button>`;
496
+ }
497
+
498
+ // Generate radius buttons
499
+ let radiusButtons = '';
500
+ this.RADIUS_OPTIONS.forEach(r => {
501
+ radiusButtons += `<button class="tc-radius-btn${r === this.state.radius ? ' is-active' : ''}" data-radius="${r}">${r}</button>`;
502
+ });
503
+
504
+ // Generate font options
505
+ let fontOptions = '';
506
+ for (const [key, value] of Object.entries(this.FONT_OPTIONS)) {
507
+ fontOptions += `<option value="${key}"${key === this.state.font ? ' selected' : ''}>${value.name}</option>`;
508
+ }
509
+
510
+ // Generate mode buttons
511
+ const modeIcons = {
512
+ 'system': 'ph-desktop',
513
+ 'dark': 'ph-moon',
514
+ 'light': 'ph-sun'
515
+ };
516
+ let modeButtons = '';
517
+ this.THEME_MODES.forEach(mode => {
518
+ modeButtons += `<button class="tc-mode-btn${mode === this.state.theme ? ' is-active' : ''}" data-mode="${mode}"><i class="ph ${modeIcons[mode]}"></i><span>${mode.charAt(0).toUpperCase() + mode.slice(1)}</span></button>`;
519
+ });
520
+
521
+ return `
522
+ <div class="tc-header">
523
+ <h3 class="tc-title">Customize Theme</h3>
524
+ <button class="customizer-mobile-close" aria-label="Close">
525
+ <i class="ph ph-x"></i>
526
+ </button>
527
+ </div>
528
+ <div class="tc-body">
529
+ <div class="tc-section">
530
+ <label class="tc-label">Color Mode</label>
531
+ <div class="tc-mode-group">
532
+ ${modeButtons}
533
+ </div>
534
+ </div>
535
+ <div class="tc-section">
536
+ <label class="tc-label">Primary Color</label>
537
+ <div class="tc-color-grid">
538
+ ${primarySwatches}
539
+ </div>
540
+ </div>
541
+ <div class="tc-section">
542
+ <label class="tc-label">Neutral Color</label>
543
+ <div class="tc-neutral-grid">
544
+ ${neutralSwatches}
545
+ </div>
546
+ </div>
547
+ <div class="tc-section">
548
+ <label class="tc-label">Border Radius</label>
549
+ <div class="tc-radius-group">
550
+ ${radiusButtons}
551
+ </div>
552
+ </div>
553
+ <div class="tc-section">
554
+ <label class="tc-label">Font Family</label>
555
+ <select class="tc-font-select" data-customizer-font>
556
+ ${fontOptions}
557
+ </select>
558
+ </div>
559
+ </div>
560
+ <div class="tc-footer">
561
+ <button class="customizer-reset btn btn-sm btn-outline">Reset to Defaults</button>
562
+ </div>
563
+ `;
564
+ },
565
+
566
+ /**
567
+ * Bind event listeners
568
+ */
569
+ /**
570
+ * Check whether the current primary color is one of the auto-defaults
571
+ * (i.e. the user hasn't explicitly picked a non-default color).
572
+ */
573
+ isUsingDefaultPrimary: function () {
574
+ return this.state.primary === this.DEFAULTS.PRIMARY_LIGHT ||
575
+ this.state.primary === this.DEFAULTS.PRIMARY_DARK;
576
+ },
577
+
578
+ bindEvents: function () {
579
+ // Trigger click - bind to any trigger button
580
+ if (this.elements.trigger) {
581
+ this.addListener(this.elements.trigger, 'click', (e) => {
582
+ e.preventDefault();
583
+ e.stopPropagation();
584
+ this.toggle();
585
+ });
586
+ }
587
+
588
+ this.bindPanelEvents();
589
+
590
+ // Listen for OS dark/light changes so we can swap the default primary
591
+ if (window.matchMedia) {
592
+ const mq = window.matchMedia('(prefers-color-scheme: dark)');
593
+ const handler = () => {
594
+ if (this.state.theme === 'system' && this.isUsingDefaultPrimary()) {
595
+ const newDefault = this.getDefaultPrimary('system');
596
+ if (newDefault !== this.state.primary) {
597
+ this.applyPrimary(newDefault);
598
+ this.updateUI();
599
+ }
600
+ }
601
+ };
602
+ mq.addEventListener('change', handler);
603
+ this._cleanup.push(() => mq.removeEventListener('change', handler));
604
+ }
605
+
606
+ // Close on outside click
607
+ this.addListener(document, 'click', (e) => {
608
+ if (this.state.isOpen && this.elements.customizer && !this.elements.customizer.contains(e.target)) {
609
+ this.close();
610
+ }
611
+ });
612
+
613
+ // Close on Escape key
614
+ this.addListener(document, 'keydown', (e) => {
615
+ if (e.key === 'Escape' && this.state.isOpen) {
616
+ this.close();
617
+ }
618
+ });
619
+ },
620
+
621
+ /**
622
+ * Toggle panel open/close
623
+ */
624
+ toggle: function () {
625
+ if (this.state.isOpen) {
626
+ this.close();
627
+ } else {
628
+ this.open();
629
+ }
630
+ },
631
+
632
+ /**
633
+ * Open the panel
634
+ */
635
+ open: function () {
636
+ this.state.isOpen = true;
637
+
638
+ // Ensure panel is positioned correctly before opening
639
+ this.positionPanel();
640
+
641
+ if (this.elements.panel) {
642
+ this.elements.panel.classList.add('is-open');
643
+ }
644
+ if (this.elements.trigger) {
645
+ this.elements.trigger.setAttribute('aria-expanded', 'true');
646
+ }
647
+ if (this.elements.overlay) {
648
+ this.elements.overlay.classList.add('is-active');
649
+ }
650
+
651
+ this.dispatchEvent('panel-open', { isOpen: true });
652
+ },
653
+
654
+ /**
655
+ * Close the panel
656
+ */
657
+ close: function () {
658
+ this.state.isOpen = false;
659
+
660
+ if (this.elements.panel) {
661
+ this.elements.panel.classList.remove('is-open');
662
+ }
663
+ if (this.elements.trigger) {
664
+ this.elements.trigger.setAttribute('aria-expanded', 'false');
665
+ }
666
+ if (this.elements.overlay) {
667
+ this.elements.overlay.classList.remove('is-active');
668
+ }
669
+
670
+ this.dispatchEvent('panel-close', { isOpen: false });
671
+ },
672
+
673
+ /**
674
+ * Update UI to reflect current state
675
+ */
676
+ updateUI: function () {
677
+ if (!this.elements.panel) return;
678
+
679
+ // Update primary color swatches
680
+ this.elements.panel.querySelectorAll('[data-color]').forEach(swatch => {
681
+ swatch.classList.toggle('is-active', swatch.dataset.color === this.state.primary);
682
+ });
683
+
684
+ // Update neutral color swatches
685
+ this.elements.panel.querySelectorAll('[data-neutral]').forEach(swatch => {
686
+ swatch.classList.toggle('is-active', swatch.dataset.neutral === this.state.neutral);
687
+ });
688
+
689
+ // Update radius buttons
690
+ this.elements.panel.querySelectorAll('[data-radius]').forEach(btn => {
691
+ btn.classList.toggle('is-active', btn.dataset.radius === this.state.radius);
692
+ });
693
+
694
+ // Update font selector
695
+ const fontSelect = this.elements.panel.querySelector('[data-customizer-font]');
696
+ if (fontSelect) {
697
+ fontSelect.value = this.state.font;
698
+ }
699
+
700
+ // Update mode buttons
701
+ this.elements.panel.querySelectorAll('[data-mode]').forEach(btn => {
702
+ btn.classList.toggle('is-active', btn.dataset.mode === this.state.theme);
703
+ });
704
+ },
705
+
706
+ /**
707
+ * Reset all preferences to defaults
708
+ */
709
+ reset: function () {
710
+ this.applyTheme(this.DEFAULTS.THEME);
711
+ this.applyPrimary(this.getDefaultPrimary(this.DEFAULTS.THEME));
712
+ this.applyNeutral(this.DEFAULTS.NEUTRAL);
713
+ this.applyRadius(this.DEFAULTS.RADIUS);
714
+ this.applyFont(this.DEFAULTS.FONT);
715
+ this.applyTheme(this.DEFAULTS.THEME);
716
+ this.updateUI();
717
+
718
+ this.dispatchEvent('reset', { state: { ...this.state } });
719
+ },
720
+
721
+ /**
722
+ * Get current state
723
+ */
724
+ getState: function () {
725
+ return { ...this.state };
726
+ },
727
+
728
+ /**
729
+ * Programmatically set preferences
730
+ */
731
+ setPreferences: function (prefs) {
732
+ if (prefs.primary) this.applyPrimary(prefs.primary);
733
+ if (prefs.neutral) this.applyNeutral(prefs.neutral);
734
+ if (prefs.radius) this.applyRadius(prefs.radius);
735
+ if (prefs.font) this.applyFont(prefs.font);
736
+ if (prefs.theme) this.applyTheme(prefs.theme);
737
+ this.updateUI();
738
+ },
739
+
740
+ getStorageValue: function (key, fallback) {
741
+ if (typeof window.safeStorageGet === 'function') {
742
+ return window.safeStorageGet(key, fallback);
743
+ }
744
+ try {
745
+ const value = localStorage.getItem(key);
746
+ return value !== null ? value : fallback;
747
+ } catch (_e) {
748
+ return fallback;
749
+ }
750
+ },
751
+
752
+ setStorageValue: function (key, value) {
753
+ if (typeof window.safeStorageSet === 'function') {
754
+ return window.safeStorageSet(key, value);
755
+ }
756
+ try {
757
+ localStorage.setItem(key, value);
758
+ return true;
759
+ } catch (_e) {
760
+ return false;
761
+ }
762
+ },
763
+
764
+ destroyAll: function () {
765
+ this._cleanup.forEach(fn => fn());
766
+ this._cleanup = [];
767
+
768
+ if (this.elements.panel) {
769
+ this.elements.panel.removeAttribute('data-customizer-initialized');
770
+ }
771
+
772
+ this.close();
773
+ this.isInitialized = false;
774
+ }
775
+ };
776
+
777
+ // Register component
778
+ if (window.Vanduo) {
779
+ window.Vanduo.register('themeCustomizer', ThemeCustomizer);
780
+ }
781
+
782
+ // Expose globally for convenience
783
+ window.ThemeCustomizer = ThemeCustomizer;
784
+ })();