@vanduo-oss/framework 1.4.3 → 1.4.4

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Vanduo Framework v1.4.3
1
+ # Vanduo Framework v1.4.4
2
2
 
3
3
  <p align="center">
4
4
  <img src="vanduo-banner.svg" alt="Vanduo Framework Banner" width="100%">
@@ -22,15 +22,23 @@ Vanduo is a zero-dependency UI framework built with HTML, CSS, and vanilla JavaS
22
22
  - Strict design token API under `--vd-*`
23
23
  - Built-in dark, light, and system theming
24
24
  - Theme customizer with color, font, and radius controls
25
+ - Theme Switcher menu variant for icon-only light/dark/system selection in navbars
25
26
  - Playwright-based browser coverage across Chromium, Firefox, and WebKit
26
27
 
28
+ ## What's New in 1.4.4
29
+
30
+ - **Theme Switcher menu variant** — `.vd-theme-switcher[data-theme-ui="menu"]` opens an icon-only picker; option click applies the theme without cycling on toggle. Cycle (`button[data-toggle="theme"]`) and select variants are unchanged.
31
+ - **New stylesheet** — `css/components/theme-switcher.css` for menu layout, active state, and navbar alignment.
32
+ - **Optional tooltips** — add `data-tooltip` on the toggle or menu options when the Tooltips component is loaded (opt-in).
33
+ - Normative API: [openspec/specs/theme-switcher/spec.md](openspec/specs/theme-switcher/spec.md).
34
+
27
35
  ## Quick Start
28
36
 
29
37
  ### CDN
30
38
 
31
39
  ```html
32
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.4.3/dist/vanduo.min.css">
33
- <script src="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.4.3/dist/vanduo.min.js"></script>
40
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.4.4/dist/vanduo.min.css">
41
+ <script src="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.4.4/dist/vanduo.min.js"></script>
34
42
  <script>
35
43
  Vanduo.init();
36
44
  </script>
@@ -114,6 +122,7 @@ framework/
114
122
  ├── dist/ # Built artifacts
115
123
  ├── tests/ # Playwright fixtures and specs
116
124
  ├── scripts/ # Build, verification, and inventory scripts
125
+ ├── openspec/ # Spec-driven change proposals and component specs
117
126
  └── docs/*.md # Release and architecture notes
118
127
  ```
119
128
 
@@ -129,11 +138,14 @@ pnpm test
129
138
  pnpm run stats:css
130
139
  ```
131
140
 
141
+ CI runs **Chromium Desktop smoke tests on pull requests** and the **full cross-browser Playwright matrix on push to `main`** (see [`.github/workflows/tests.yml`](.github/workflows/tests.yml) and [QA-Automation-Strategy.md](QA-Automation-Strategy.md)).
142
+
132
143
  ## Release Notes
133
144
 
134
145
  - Architecture notes: [ARCHITECTURE.md](ARCHITECTURE.md)
135
146
  - Token model: [TOKENS.md](TOKENS.md)
136
147
  - `1.4.1` token migration notes: [changes-v141.md](changes-v141.md)
148
+ - Theme Switcher spec: [openspec/specs/theme-switcher/spec.md](openspec/specs/theme-switcher/spec.md)
137
149
  - QA strategy: [QA-Automation-Strategy.md](QA-Automation-Strategy.md)
138
150
  - Contributor workflow: [CONTRIBUTING.md](CONTRIBUTING.md)
139
151
 
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Vanduo Framework - Theme Switcher
3
+ * Icon-only menu variant for explicit theme selection
4
+ */
5
+
6
+ :root {
7
+ --vd-theme-switcher-menu-bg: var(--vd-color-white);
8
+ --vd-theme-switcher-menu-border-color: var(--vd-border-color);
9
+ --vd-theme-switcher-option-hover-bg: var(--vd-bg-secondary);
10
+ --vd-theme-switcher-option-active-bg: var(--vd-color-primary-alpha-10);
11
+ --vd-theme-switcher-option-active-color: var(--vd-color-primary);
12
+ --vd-theme-switcher-z-index: 1000;
13
+ }
14
+
15
+ [data-theme="dark"] {
16
+ --vd-theme-switcher-menu-bg: var(--vd-bg-primary);
17
+ --vd-theme-switcher-option-hover-bg: var(--vd-bg-secondary);
18
+ }
19
+
20
+ @media (prefers-color-scheme: dark) {
21
+ :root:not([data-theme]) {
22
+ --vd-theme-switcher-menu-bg: var(--vd-bg-primary);
23
+ --vd-theme-switcher-option-hover-bg: var(--vd-bg-secondary);
24
+ }
25
+ }
26
+
27
+ .vd-theme-switcher {
28
+ position: relative;
29
+ display: inline-block;
30
+ }
31
+
32
+ .vd-theme-switcher-toggle {
33
+ display: inline-flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ width: 2.375rem;
37
+ height: 2.375rem;
38
+ padding: 0;
39
+ border: 1px solid var(--vd-border-color);
40
+ border-radius: var(--vd-btn-border-radius, var(--vd-card-border-radius));
41
+ background: transparent;
42
+ color: var(--vd-text-secondary);
43
+ cursor: pointer;
44
+ font-size: 1.15rem;
45
+ line-height: 1;
46
+ transition: color 0.2s, border-color 0.2s, background 0.2s;
47
+ }
48
+
49
+ .vd-theme-switcher-toggle:hover,
50
+ .vd-theme-switcher-toggle:focus-visible {
51
+ color: var(--vd-color-primary);
52
+ border-color: var(--vd-color-primary);
53
+ background: var(--vd-color-primary-alpha-10);
54
+ outline: none;
55
+ }
56
+
57
+ .vd-theme-switcher-menu {
58
+ position: absolute;
59
+ top: calc(100% + 0.375rem);
60
+ left: 0;
61
+ z-index: var(--vd-theme-switcher-z-index);
62
+ display: none;
63
+ flex-direction: column;
64
+ gap: 0.25rem;
65
+ min-width: auto;
66
+ padding: 0.375rem;
67
+ margin: 0;
68
+ background: var(--vd-theme-switcher-menu-bg, #ffffff);
69
+ border: 1px solid var(--vd-theme-switcher-menu-border-color);
70
+ border-radius: var(--vd-btn-border-radius, var(--vd-card-border-radius));
71
+ box-shadow: var(--vd-shadow-lg);
72
+ list-style: none;
73
+ isolation: isolate;
74
+ backdrop-filter: none;
75
+ -webkit-backdrop-filter: none;
76
+ visibility: hidden;
77
+ pointer-events: none;
78
+ transform: translateY(-6px);
79
+ transition: visibility var(--vd-transition-duration-base, 0.2s) var(--vd-transition-ease, ease),
80
+ transform var(--vd-transition-duration-base, 0.2s) var(--vd-transition-ease, ease);
81
+ }
82
+
83
+ .vd-theme-switcher-menu-end .vd-theme-switcher-menu,
84
+ .vd-theme-switcher.vd-theme-switcher-menu-end .vd-theme-switcher-menu {
85
+ right: 0;
86
+ left: auto;
87
+ }
88
+
89
+ .vd-theme-switcher.is-open .vd-theme-switcher-menu,
90
+ .vd-theme-switcher-menu.is-open {
91
+ display: flex;
92
+ visibility: visible;
93
+ pointer-events: auto;
94
+ transform: translateY(0);
95
+ }
96
+
97
+ .vd-theme-switcher-option,
98
+ .vd-theme-switcher-menu [data-theme-value] {
99
+ display: inline-flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ width: 2.375rem;
103
+ height: 2.375rem;
104
+ padding: 0;
105
+ border: 1px solid transparent;
106
+ border-radius: var(--vd-btn-border-radius-sm, var(--vd-radius-fib-3));
107
+ background: transparent;
108
+ color: var(--vd-text-secondary);
109
+ cursor: pointer;
110
+ font-size: 1.15rem;
111
+ line-height: 1;
112
+ transition: color 0.2s, background 0.2s, border-color 0.2s;
113
+ }
114
+
115
+ .vd-theme-switcher-option:hover,
116
+ .vd-theme-switcher-option:focus-visible,
117
+ .vd-theme-switcher-menu [data-theme-value]:hover,
118
+ .vd-theme-switcher-menu [data-theme-value]:focus-visible {
119
+ color: var(--vd-text-primary);
120
+ background: var(--vd-theme-switcher-option-hover-bg);
121
+ outline: none;
122
+ }
123
+
124
+ .vd-theme-switcher-option.is-active,
125
+ .vd-theme-switcher-menu [data-theme-value].is-active {
126
+ color: var(--vd-theme-switcher-option-active-color);
127
+ background: var(--vd-theme-switcher-option-active-bg);
128
+ border-color: var(--vd-color-primary-alpha-20, transparent);
129
+ }
package/css/vanduo.css CHANGED
@@ -53,6 +53,7 @@
53
53
  @import url('components/spinner.css');
54
54
  @import url('components/button-group.css');
55
55
  @import url('components/theme-customizer.css');
56
+ @import url('components/theme-switcher.css');
56
57
  @import url('components/image-box.css');
57
58
  @import url('components/doc-tabs.css');
58
59
  @import url('components/doc-search.css');
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.4.3",
3
- "builtAt": "2026-05-28T13:14:25.371Z",
4
- "commit": "02ef7f6",
2
+ "version": "1.4.4",
3
+ "builtAt": "2026-06-07T19:14:20.877Z",
4
+ "commit": "781df0d",
5
5
  "mode": "development+production"
6
6
  }
@@ -1,4 +1,4 @@
1
- /*! Vanduo v1.4.3 | Built: 2026-05-28T13:14:25.371Z | git:02ef7f6 | development */
1
+ /*! Vanduo v1.4.4 | Built: 2026-06-07T19:14:20.877Z | git:781df0d | development */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -201,7 +201,7 @@ module.exports = __toCommonJS(index_exports);
201
201
  // js/vanduo.js
202
202
  (function() {
203
203
  "use strict";
204
- const VANDUO_VERSION = true ? "1.4.3" : "0.0.0-dev";
204
+ const VANDUO_VERSION = true ? "1.4.4" : "0.0.0-dev";
205
205
  const hasOwn = Object.prototype.hasOwnProperty;
206
206
  const Vanduo2 = {
207
207
  version: VANDUO_VERSION,
@@ -486,9 +486,10 @@ module.exports = __toCommonJS(index_exports);
486
486
  init: function(root) {
487
487
  const snippets = this.queryWithin(root, ".vd-code-snippet");
488
488
  snippets.forEach((snippet) => {
489
- if (!snippet.dataset.initialized) {
490
- this.initSnippet(snippet);
489
+ if (snippet.dataset.initialized === "true") {
490
+ return;
491
491
  }
492
+ this.initSnippet(snippet);
492
493
  });
493
494
  },
494
495
  /**
@@ -496,7 +497,9 @@ module.exports = __toCommonJS(index_exports);
496
497
  * @param {HTMLElement} snippet - Code snippet container element
497
498
  */
498
499
  initSnippet: function(snippet) {
499
- snippet.dataset.initialized = "true";
500
+ if (snippet.dataset.initialized === "true") {
501
+ return;
502
+ }
500
503
  snippet._codeSnippetCleanup = [];
501
504
  const toggle = snippet.querySelector(".vd-code-snippet-toggle");
502
505
  const content = snippet.querySelector(".vd-code-snippet-content");
@@ -524,6 +527,7 @@ module.exports = __toCommonJS(index_exports);
524
527
  lineNumberPanes.forEach((pane) => {
525
528
  this.addLineNumbers(pane);
526
529
  });
530
+ snippet.dataset.initialized = "true";
527
531
  },
528
532
  /**
529
533
  * Initialize collapsible functionality
@@ -533,13 +537,14 @@ module.exports = __toCommonJS(index_exports);
533
537
  */
534
538
  initCollapsible: function(snippet, toggle, content) {
535
539
  const isExpanded = snippet.dataset.expanded === "true";
536
- toggle.setAttribute("aria-expanded", isExpanded);
537
- content.dataset.visible = isExpanded;
540
+ toggle.setAttribute("aria-expanded", isExpanded ? "true" : "false");
541
+ content.dataset.visible = isExpanded ? "true" : "false";
538
542
  this.addListener(snippet, toggle, "click", () => {
539
543
  const expanded = snippet.dataset.expanded === "true";
540
- snippet.dataset.expanded = !expanded;
541
- toggle.setAttribute("aria-expanded", !expanded);
542
- content.dataset.visible = !expanded;
544
+ const nextExpanded = !expanded;
545
+ snippet.dataset.expanded = nextExpanded ? "true" : "false";
546
+ toggle.setAttribute("aria-expanded", nextExpanded ? "true" : "false");
547
+ content.dataset.visible = nextExpanded ? "true" : "false";
543
548
  if (!expanded) {
544
549
  const extractPanes = content.querySelectorAll("[data-extract]:not([data-extracted])");
545
550
  extractPanes.forEach((pane) => {
@@ -5070,21 +5075,45 @@ module.exports = __toCommonJS(index_exports);
5070
5075
  // js/components/theme-switcher.js
5071
5076
  (function() {
5072
5077
  "use strict";
5078
+ const THEME_MODES = ["system", "light", "dark"];
5079
+ const THEME_ICON_CLASSES = {
5080
+ system: "ph ph-desktop",
5081
+ light: "ph ph-sun",
5082
+ dark: "ph ph-moon"
5083
+ };
5084
+ const THEME_LABELS = {
5085
+ system: "Theme: System",
5086
+ light: "Theme: Light",
5087
+ dark: "Theme: Dark"
5088
+ };
5089
+ const THEME_OPTION_TOOLTIPS = {
5090
+ system: "Use system preference",
5091
+ light: "Light theme",
5092
+ dark: "Dark theme"
5093
+ };
5073
5094
  const ThemeSwitcher = {
5074
5095
  isInitialized: false,
5075
5096
  _mediaQuery: null,
5076
5097
  _onMediaChange: null,
5098
+ menuInstances: /* @__PURE__ */ new Map(),
5077
5099
  getToggles: function(root) {
5100
+ const scope = root || document;
5101
+ const toggles = window.Vanduo && typeof window.Vanduo.queryAll === "function" ? window.Vanduo.queryAll(scope, '[data-toggle="theme"]') : Array.from(scope.querySelectorAll('[data-toggle="theme"]'));
5102
+ return toggles.filter(function(toggle) {
5103
+ return !toggle.closest('.vd-theme-switcher[data-theme-ui="menu"]');
5104
+ });
5105
+ },
5106
+ getMenuSwitchers: function(root) {
5107
+ const scope = root || document;
5078
5108
  if (window.Vanduo && typeof window.Vanduo.queryAll === "function") {
5079
- return window.Vanduo.queryAll(root, '[data-toggle="theme"]');
5109
+ return window.Vanduo.queryAll(scope, '.vd-theme-switcher[data-theme-ui="menu"]');
5080
5110
  }
5081
- return Array.from(document.querySelectorAll('[data-toggle="theme"]'));
5111
+ return Array.from(scope.querySelectorAll('.vd-theme-switcher[data-theme-ui="menu"]'));
5082
5112
  },
5083
5113
  init: function(root) {
5084
5114
  this.STORAGE_KEY = "vanduo-theme-preference";
5085
5115
  this.state = {
5086
5116
  preference: this.getPreference()
5087
- // 'light', 'dark', or 'system'
5088
5117
  };
5089
5118
  if (this.isInitialized) {
5090
5119
  this.applyTheme();
@@ -5101,6 +5130,9 @@ module.exports = __toCommonJS(index_exports);
5101
5130
  return this.getStorageValue(this.STORAGE_KEY, "system");
5102
5131
  },
5103
5132
  setPreference: function(pref) {
5133
+ if (!THEME_MODES.includes(pref)) {
5134
+ return;
5135
+ }
5104
5136
  this.state.preference = pref;
5105
5137
  this.setStorageValue(this.STORAGE_KEY, pref);
5106
5138
  this.applyTheme();
@@ -5154,8 +5186,8 @@ module.exports = __toCommonJS(index_exports);
5154
5186
  };
5155
5187
  this._mediaQuery.addEventListener("change", this._onMediaChange);
5156
5188
  },
5157
- // Helper to facilitate UI creation if needed, though often UI is in HTML
5158
5189
  renderUI: function(root) {
5190
+ this.renderMenuSwitchers(root);
5159
5191
  const toggles = this.getToggles(root);
5160
5192
  toggles.forEach((toggle) => {
5161
5193
  if (toggle.getAttribute("data-theme-initialized") === "true") {
@@ -5173,7 +5205,7 @@ module.exports = __toCommonJS(index_exports);
5173
5205
  toggle._themeToggleHandler = onChange;
5174
5206
  } else {
5175
5207
  const onClick = () => {
5176
- const modes = ["system", "light", "dark"];
5208
+ const modes = THEME_MODES;
5177
5209
  const nextIndex = (modes.indexOf(this.state.preference) + 1) % modes.length;
5178
5210
  this.setPreference(modes[nextIndex]);
5179
5211
  };
@@ -5182,6 +5214,188 @@ module.exports = __toCommonJS(index_exports);
5182
5214
  }
5183
5215
  toggle.setAttribute("data-theme-initialized", "true");
5184
5216
  });
5217
+ this.updateUI(root);
5218
+ },
5219
+ renderMenuSwitchers: function(root) {
5220
+ const switchers = this.getMenuSwitchers(root);
5221
+ switchers.forEach((switcher) => {
5222
+ if (switcher.getAttribute("data-theme-menu-initialized") === "true") {
5223
+ return;
5224
+ }
5225
+ const toggle = switcher.querySelector(".vd-theme-switcher-toggle");
5226
+ const menu = switcher.querySelector(".vd-theme-switcher-menu");
5227
+ if (!toggle || !menu) {
5228
+ return;
5229
+ }
5230
+ const options = menu.querySelectorAll("[data-theme-value]");
5231
+ const cleanupFunctions = [];
5232
+ toggle.setAttribute("aria-haspopup", "true");
5233
+ toggle.setAttribute("aria-expanded", "false");
5234
+ menu.setAttribute("aria-hidden", "true");
5235
+ const toggleClickHandler = (e) => {
5236
+ e.preventDefault();
5237
+ e.stopPropagation();
5238
+ this.toggleMenu(switcher, toggle, menu);
5239
+ };
5240
+ toggle.addEventListener("click", toggleClickHandler);
5241
+ cleanupFunctions.push(() => toggle.removeEventListener("click", toggleClickHandler));
5242
+ options.forEach((option) => {
5243
+ const optionClickHandler = (e) => {
5244
+ e.preventDefault();
5245
+ e.stopPropagation();
5246
+ const value = option.getAttribute("data-theme-value");
5247
+ if (value) {
5248
+ this.setPreference(value);
5249
+ }
5250
+ this.closeMenu(switcher, toggle, menu);
5251
+ };
5252
+ option.addEventListener("click", optionClickHandler);
5253
+ cleanupFunctions.push(() => option.removeEventListener("click", optionClickHandler));
5254
+ const optionKeydownHandler = (e) => {
5255
+ if (e.key === "Enter" || e.key === " ") {
5256
+ e.preventDefault();
5257
+ optionClickHandler(e);
5258
+ }
5259
+ };
5260
+ option.addEventListener("keydown", optionKeydownHandler);
5261
+ cleanupFunctions.push(() => option.removeEventListener("keydown", optionKeydownHandler));
5262
+ });
5263
+ const toggleKeydownHandler = (e) => {
5264
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
5265
+ e.preventDefault();
5266
+ if (!menu.classList.contains("is-open")) {
5267
+ this.openMenu(switcher, toggle, menu);
5268
+ }
5269
+ } else if (e.key === "Escape" && menu.classList.contains("is-open")) {
5270
+ e.preventDefault();
5271
+ this.closeMenu(switcher, toggle, menu);
5272
+ }
5273
+ };
5274
+ toggle.addEventListener("keydown", toggleKeydownHandler);
5275
+ cleanupFunctions.push(() => toggle.removeEventListener("keydown", toggleKeydownHandler));
5276
+ const menuKeydownHandler = (e) => {
5277
+ this.handleMenuKeydown(e, switcher, toggle, menu, options);
5278
+ };
5279
+ menu.addEventListener("keydown", menuKeydownHandler);
5280
+ cleanupFunctions.push(() => menu.removeEventListener("keydown", menuKeydownHandler));
5281
+ const documentClickHandler = (e) => {
5282
+ if (!switcher.contains(e.target) && menu.classList.contains("is-open")) {
5283
+ this.closeMenu(switcher, toggle, menu);
5284
+ }
5285
+ };
5286
+ document.addEventListener("click", documentClickHandler);
5287
+ cleanupFunctions.push(() => document.removeEventListener("click", documentClickHandler));
5288
+ this.menuInstances.set(switcher, { toggle, menu, cleanup: cleanupFunctions });
5289
+ switcher.setAttribute("data-theme-menu-initialized", "true");
5290
+ this.initMenuTooltips(switcher);
5291
+ });
5292
+ },
5293
+ initMenuTooltips: function(switcher) {
5294
+ const tooltips = window.Vanduo && typeof window.Vanduo.getComponent === "function" ? window.Vanduo.getComponent("tooltips") : null;
5295
+ if (tooltips && typeof tooltips.init === "function") {
5296
+ tooltips.init(switcher);
5297
+ }
5298
+ },
5299
+ closeOtherMenus: function(exceptMenu) {
5300
+ this.menuInstances.forEach((instance, switcher) => {
5301
+ if (instance.menu !== exceptMenu && instance.menu.classList.contains("is-open")) {
5302
+ this.closeMenu(switcher, instance.toggle, instance.menu);
5303
+ }
5304
+ });
5305
+ },
5306
+ toggleMenu: function(switcher, toggle, menu) {
5307
+ if (menu.classList.contains("is-open")) {
5308
+ this.closeMenu(switcher, toggle, menu);
5309
+ } else {
5310
+ this.openMenu(switcher, toggle, menu);
5311
+ }
5312
+ },
5313
+ openMenu: function(switcher, toggle, menu) {
5314
+ this.closeOtherMenus(menu);
5315
+ switcher.classList.add("is-open");
5316
+ menu.classList.add("is-open");
5317
+ toggle.setAttribute("aria-expanded", "true");
5318
+ menu.setAttribute("aria-hidden", "false");
5319
+ const activeOption = menu.querySelector("[data-theme-value].is-active") || menu.querySelector("[data-theme-value]");
5320
+ if (activeOption) {
5321
+ setTimeout(() => activeOption.focus(), 0);
5322
+ }
5323
+ },
5324
+ closeMenu: function(switcher, toggle, menu) {
5325
+ switcher.classList.remove("is-open");
5326
+ menu.classList.remove("is-open");
5327
+ toggle.setAttribute("aria-expanded", "false");
5328
+ menu.setAttribute("aria-hidden", "true");
5329
+ },
5330
+ handleMenuKeydown: function(e, switcher, toggle, menu, options) {
5331
+ const items = Array.from(options);
5332
+ const currentIndex = items.indexOf(document.activeElement);
5333
+ if (e.key === "Escape") {
5334
+ e.preventDefault();
5335
+ this.closeMenu(switcher, toggle, menu);
5336
+ toggle.focus();
5337
+ return;
5338
+ }
5339
+ if (e.key === "ArrowDown") {
5340
+ e.preventDefault();
5341
+ const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
5342
+ items[nextIndex].focus();
5343
+ return;
5344
+ }
5345
+ if (e.key === "ArrowUp") {
5346
+ e.preventDefault();
5347
+ const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
5348
+ items[prevIndex].focus();
5349
+ }
5350
+ },
5351
+ updateMenuSwitcher: function(switcher) {
5352
+ const toggle = switcher.querySelector(".vd-theme-switcher-toggle");
5353
+ const menu = switcher.querySelector(".vd-theme-switcher-menu");
5354
+ if (!toggle || !menu) {
5355
+ return;
5356
+ }
5357
+ const pref = this.state.preference;
5358
+ const icon = toggle.querySelector("[data-theme-icon]");
5359
+ const label = THEME_LABELS[pref] || THEME_LABELS.system;
5360
+ if (icon) {
5361
+ icon.className = THEME_ICON_CLASSES[pref] || THEME_ICON_CLASSES.system;
5362
+ }
5363
+ toggle.setAttribute("aria-label", label);
5364
+ if (toggle.hasAttribute("data-tooltip")) {
5365
+ toggle.setAttribute("data-tooltip", label);
5366
+ this.refreshTooltipContent(toggle, label);
5367
+ }
5368
+ menu.querySelectorAll("[data-theme-value]").forEach((option) => {
5369
+ const value = option.getAttribute("data-theme-value");
5370
+ const isActive = value === pref;
5371
+ option.classList.toggle("is-active", isActive);
5372
+ option.setAttribute("aria-checked", isActive ? "true" : "false");
5373
+ const tooltipText = THEME_OPTION_TOOLTIPS[value];
5374
+ if (tooltipText && option.hasAttribute("data-tooltip")) {
5375
+ option.setAttribute("data-tooltip", tooltipText);
5376
+ this.refreshTooltipContent(option, tooltipText);
5377
+ }
5378
+ });
5379
+ },
5380
+ refreshTooltipContent: function(element, text) {
5381
+ const tooltips = window.Vanduo && typeof window.Vanduo.getComponent === "function" ? window.Vanduo.getComponent("tooltips") : null;
5382
+ if (!tooltips || typeof tooltips.update !== "function") {
5383
+ return;
5384
+ }
5385
+ tooltips.update(element, text);
5386
+ },
5387
+ updateCycleToggle: function(toggle) {
5388
+ const pref = this.state.preference;
5389
+ const icon = toggle.querySelector("[data-theme-icon]");
5390
+ const label = THEME_LABELS[pref] || THEME_LABELS.system;
5391
+ if (icon) {
5392
+ icon.className = THEME_ICON_CLASSES[pref] || THEME_ICON_CLASSES.system;
5393
+ }
5394
+ toggle.setAttribute("aria-label", label);
5395
+ if (toggle.hasAttribute("data-tooltip")) {
5396
+ toggle.setAttribute("data-tooltip", label);
5397
+ this.refreshTooltipContent(toggle, label);
5398
+ }
5185
5399
  },
5186
5400
  updateUI: function(root) {
5187
5401
  const toggles = this.getToggles(root);
@@ -5193,11 +5407,26 @@ module.exports = __toCommonJS(index_exports);
5193
5407
  if (span) {
5194
5408
  span.textContent = this.state.preference.charAt(0).toUpperCase() + this.state.preference.slice(1);
5195
5409
  }
5410
+ if (toggle.querySelector("[data-theme-icon]")) {
5411
+ this.updateCycleToggle(toggle);
5412
+ }
5196
5413
  }
5197
5414
  });
5415
+ this.getMenuSwitchers(root).forEach((switcher) => {
5416
+ this.updateMenuSwitcher(switcher);
5417
+ });
5198
5418
  },
5199
5419
  destroyAll: function(root) {
5200
5420
  const scope = root || document;
5421
+ this.getMenuSwitchers(scope).forEach((switcher) => {
5422
+ const instance = this.menuInstances.get(switcher);
5423
+ if (instance) {
5424
+ instance.cleanup.forEach((fn) => fn());
5425
+ this.closeMenu(switcher, instance.toggle, instance.menu);
5426
+ this.menuInstances.delete(switcher);
5427
+ }
5428
+ switcher.removeAttribute("data-theme-menu-initialized");
5429
+ });
5201
5430
  const toggles = this.getToggles(scope).filter(function(toggle) {
5202
5431
  return toggle.getAttribute("data-theme-initialized") === "true";
5203
5432
  });