@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/dist/vanduo.js CHANGED
@@ -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
  (() => {
3
3
  // js/utils/lifecycle.js
4
4
  (function() {
@@ -176,7 +176,7 @@
176
176
  // js/vanduo.js
177
177
  (function() {
178
178
  "use strict";
179
- const VANDUO_VERSION = true ? "1.4.3" : "0.0.0-dev";
179
+ const VANDUO_VERSION = true ? "1.4.4" : "0.0.0-dev";
180
180
  const hasOwn = Object.prototype.hasOwnProperty;
181
181
  const Vanduo2 = {
182
182
  version: VANDUO_VERSION,
@@ -461,9 +461,10 @@
461
461
  init: function(root) {
462
462
  const snippets = this.queryWithin(root, ".vd-code-snippet");
463
463
  snippets.forEach((snippet) => {
464
- if (!snippet.dataset.initialized) {
465
- this.initSnippet(snippet);
464
+ if (snippet.dataset.initialized === "true") {
465
+ return;
466
466
  }
467
+ this.initSnippet(snippet);
467
468
  });
468
469
  },
469
470
  /**
@@ -471,7 +472,9 @@
471
472
  * @param {HTMLElement} snippet - Code snippet container element
472
473
  */
473
474
  initSnippet: function(snippet) {
474
- snippet.dataset.initialized = "true";
475
+ if (snippet.dataset.initialized === "true") {
476
+ return;
477
+ }
475
478
  snippet._codeSnippetCleanup = [];
476
479
  const toggle = snippet.querySelector(".vd-code-snippet-toggle");
477
480
  const content = snippet.querySelector(".vd-code-snippet-content");
@@ -499,6 +502,7 @@
499
502
  lineNumberPanes.forEach((pane) => {
500
503
  this.addLineNumbers(pane);
501
504
  });
505
+ snippet.dataset.initialized = "true";
502
506
  },
503
507
  /**
504
508
  * Initialize collapsible functionality
@@ -508,13 +512,14 @@
508
512
  */
509
513
  initCollapsible: function(snippet, toggle, content) {
510
514
  const isExpanded = snippet.dataset.expanded === "true";
511
- toggle.setAttribute("aria-expanded", isExpanded);
512
- content.dataset.visible = isExpanded;
515
+ toggle.setAttribute("aria-expanded", isExpanded ? "true" : "false");
516
+ content.dataset.visible = isExpanded ? "true" : "false";
513
517
  this.addListener(snippet, toggle, "click", () => {
514
518
  const expanded = snippet.dataset.expanded === "true";
515
- snippet.dataset.expanded = !expanded;
516
- toggle.setAttribute("aria-expanded", !expanded);
517
- content.dataset.visible = !expanded;
519
+ const nextExpanded = !expanded;
520
+ snippet.dataset.expanded = nextExpanded ? "true" : "false";
521
+ toggle.setAttribute("aria-expanded", nextExpanded ? "true" : "false");
522
+ content.dataset.visible = nextExpanded ? "true" : "false";
518
523
  if (!expanded) {
519
524
  const extractPanes = content.querySelectorAll("[data-extract]:not([data-extracted])");
520
525
  extractPanes.forEach((pane) => {
@@ -5045,21 +5050,45 @@
5045
5050
  // js/components/theme-switcher.js
5046
5051
  (function() {
5047
5052
  "use strict";
5053
+ const THEME_MODES = ["system", "light", "dark"];
5054
+ const THEME_ICON_CLASSES = {
5055
+ system: "ph ph-desktop",
5056
+ light: "ph ph-sun",
5057
+ dark: "ph ph-moon"
5058
+ };
5059
+ const THEME_LABELS = {
5060
+ system: "Theme: System",
5061
+ light: "Theme: Light",
5062
+ dark: "Theme: Dark"
5063
+ };
5064
+ const THEME_OPTION_TOOLTIPS = {
5065
+ system: "Use system preference",
5066
+ light: "Light theme",
5067
+ dark: "Dark theme"
5068
+ };
5048
5069
  const ThemeSwitcher = {
5049
5070
  isInitialized: false,
5050
5071
  _mediaQuery: null,
5051
5072
  _onMediaChange: null,
5073
+ menuInstances: /* @__PURE__ */ new Map(),
5052
5074
  getToggles: function(root) {
5075
+ const scope = root || document;
5076
+ const toggles = window.Vanduo && typeof window.Vanduo.queryAll === "function" ? window.Vanduo.queryAll(scope, '[data-toggle="theme"]') : Array.from(scope.querySelectorAll('[data-toggle="theme"]'));
5077
+ return toggles.filter(function(toggle) {
5078
+ return !toggle.closest('.vd-theme-switcher[data-theme-ui="menu"]');
5079
+ });
5080
+ },
5081
+ getMenuSwitchers: function(root) {
5082
+ const scope = root || document;
5053
5083
  if (window.Vanduo && typeof window.Vanduo.queryAll === "function") {
5054
- return window.Vanduo.queryAll(root, '[data-toggle="theme"]');
5084
+ return window.Vanduo.queryAll(scope, '.vd-theme-switcher[data-theme-ui="menu"]');
5055
5085
  }
5056
- return Array.from(document.querySelectorAll('[data-toggle="theme"]'));
5086
+ return Array.from(scope.querySelectorAll('.vd-theme-switcher[data-theme-ui="menu"]'));
5057
5087
  },
5058
5088
  init: function(root) {
5059
5089
  this.STORAGE_KEY = "vanduo-theme-preference";
5060
5090
  this.state = {
5061
5091
  preference: this.getPreference()
5062
- // 'light', 'dark', or 'system'
5063
5092
  };
5064
5093
  if (this.isInitialized) {
5065
5094
  this.applyTheme();
@@ -5076,6 +5105,9 @@
5076
5105
  return this.getStorageValue(this.STORAGE_KEY, "system");
5077
5106
  },
5078
5107
  setPreference: function(pref) {
5108
+ if (!THEME_MODES.includes(pref)) {
5109
+ return;
5110
+ }
5079
5111
  this.state.preference = pref;
5080
5112
  this.setStorageValue(this.STORAGE_KEY, pref);
5081
5113
  this.applyTheme();
@@ -5129,8 +5161,8 @@
5129
5161
  };
5130
5162
  this._mediaQuery.addEventListener("change", this._onMediaChange);
5131
5163
  },
5132
- // Helper to facilitate UI creation if needed, though often UI is in HTML
5133
5164
  renderUI: function(root) {
5165
+ this.renderMenuSwitchers(root);
5134
5166
  const toggles = this.getToggles(root);
5135
5167
  toggles.forEach((toggle) => {
5136
5168
  if (toggle.getAttribute("data-theme-initialized") === "true") {
@@ -5148,7 +5180,7 @@
5148
5180
  toggle._themeToggleHandler = onChange;
5149
5181
  } else {
5150
5182
  const onClick = () => {
5151
- const modes = ["system", "light", "dark"];
5183
+ const modes = THEME_MODES;
5152
5184
  const nextIndex = (modes.indexOf(this.state.preference) + 1) % modes.length;
5153
5185
  this.setPreference(modes[nextIndex]);
5154
5186
  };
@@ -5157,6 +5189,188 @@
5157
5189
  }
5158
5190
  toggle.setAttribute("data-theme-initialized", "true");
5159
5191
  });
5192
+ this.updateUI(root);
5193
+ },
5194
+ renderMenuSwitchers: function(root) {
5195
+ const switchers = this.getMenuSwitchers(root);
5196
+ switchers.forEach((switcher) => {
5197
+ if (switcher.getAttribute("data-theme-menu-initialized") === "true") {
5198
+ return;
5199
+ }
5200
+ const toggle = switcher.querySelector(".vd-theme-switcher-toggle");
5201
+ const menu = switcher.querySelector(".vd-theme-switcher-menu");
5202
+ if (!toggle || !menu) {
5203
+ return;
5204
+ }
5205
+ const options = menu.querySelectorAll("[data-theme-value]");
5206
+ const cleanupFunctions = [];
5207
+ toggle.setAttribute("aria-haspopup", "true");
5208
+ toggle.setAttribute("aria-expanded", "false");
5209
+ menu.setAttribute("aria-hidden", "true");
5210
+ const toggleClickHandler = (e) => {
5211
+ e.preventDefault();
5212
+ e.stopPropagation();
5213
+ this.toggleMenu(switcher, toggle, menu);
5214
+ };
5215
+ toggle.addEventListener("click", toggleClickHandler);
5216
+ cleanupFunctions.push(() => toggle.removeEventListener("click", toggleClickHandler));
5217
+ options.forEach((option) => {
5218
+ const optionClickHandler = (e) => {
5219
+ e.preventDefault();
5220
+ e.stopPropagation();
5221
+ const value = option.getAttribute("data-theme-value");
5222
+ if (value) {
5223
+ this.setPreference(value);
5224
+ }
5225
+ this.closeMenu(switcher, toggle, menu);
5226
+ };
5227
+ option.addEventListener("click", optionClickHandler);
5228
+ cleanupFunctions.push(() => option.removeEventListener("click", optionClickHandler));
5229
+ const optionKeydownHandler = (e) => {
5230
+ if (e.key === "Enter" || e.key === " ") {
5231
+ e.preventDefault();
5232
+ optionClickHandler(e);
5233
+ }
5234
+ };
5235
+ option.addEventListener("keydown", optionKeydownHandler);
5236
+ cleanupFunctions.push(() => option.removeEventListener("keydown", optionKeydownHandler));
5237
+ });
5238
+ const toggleKeydownHandler = (e) => {
5239
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
5240
+ e.preventDefault();
5241
+ if (!menu.classList.contains("is-open")) {
5242
+ this.openMenu(switcher, toggle, menu);
5243
+ }
5244
+ } else if (e.key === "Escape" && menu.classList.contains("is-open")) {
5245
+ e.preventDefault();
5246
+ this.closeMenu(switcher, toggle, menu);
5247
+ }
5248
+ };
5249
+ toggle.addEventListener("keydown", toggleKeydownHandler);
5250
+ cleanupFunctions.push(() => toggle.removeEventListener("keydown", toggleKeydownHandler));
5251
+ const menuKeydownHandler = (e) => {
5252
+ this.handleMenuKeydown(e, switcher, toggle, menu, options);
5253
+ };
5254
+ menu.addEventListener("keydown", menuKeydownHandler);
5255
+ cleanupFunctions.push(() => menu.removeEventListener("keydown", menuKeydownHandler));
5256
+ const documentClickHandler = (e) => {
5257
+ if (!switcher.contains(e.target) && menu.classList.contains("is-open")) {
5258
+ this.closeMenu(switcher, toggle, menu);
5259
+ }
5260
+ };
5261
+ document.addEventListener("click", documentClickHandler);
5262
+ cleanupFunctions.push(() => document.removeEventListener("click", documentClickHandler));
5263
+ this.menuInstances.set(switcher, { toggle, menu, cleanup: cleanupFunctions });
5264
+ switcher.setAttribute("data-theme-menu-initialized", "true");
5265
+ this.initMenuTooltips(switcher);
5266
+ });
5267
+ },
5268
+ initMenuTooltips: function(switcher) {
5269
+ const tooltips = window.Vanduo && typeof window.Vanduo.getComponent === "function" ? window.Vanduo.getComponent("tooltips") : null;
5270
+ if (tooltips && typeof tooltips.init === "function") {
5271
+ tooltips.init(switcher);
5272
+ }
5273
+ },
5274
+ closeOtherMenus: function(exceptMenu) {
5275
+ this.menuInstances.forEach((instance, switcher) => {
5276
+ if (instance.menu !== exceptMenu && instance.menu.classList.contains("is-open")) {
5277
+ this.closeMenu(switcher, instance.toggle, instance.menu);
5278
+ }
5279
+ });
5280
+ },
5281
+ toggleMenu: function(switcher, toggle, menu) {
5282
+ if (menu.classList.contains("is-open")) {
5283
+ this.closeMenu(switcher, toggle, menu);
5284
+ } else {
5285
+ this.openMenu(switcher, toggle, menu);
5286
+ }
5287
+ },
5288
+ openMenu: function(switcher, toggle, menu) {
5289
+ this.closeOtherMenus(menu);
5290
+ switcher.classList.add("is-open");
5291
+ menu.classList.add("is-open");
5292
+ toggle.setAttribute("aria-expanded", "true");
5293
+ menu.setAttribute("aria-hidden", "false");
5294
+ const activeOption = menu.querySelector("[data-theme-value].is-active") || menu.querySelector("[data-theme-value]");
5295
+ if (activeOption) {
5296
+ setTimeout(() => activeOption.focus(), 0);
5297
+ }
5298
+ },
5299
+ closeMenu: function(switcher, toggle, menu) {
5300
+ switcher.classList.remove("is-open");
5301
+ menu.classList.remove("is-open");
5302
+ toggle.setAttribute("aria-expanded", "false");
5303
+ menu.setAttribute("aria-hidden", "true");
5304
+ },
5305
+ handleMenuKeydown: function(e, switcher, toggle, menu, options) {
5306
+ const items = Array.from(options);
5307
+ const currentIndex = items.indexOf(document.activeElement);
5308
+ if (e.key === "Escape") {
5309
+ e.preventDefault();
5310
+ this.closeMenu(switcher, toggle, menu);
5311
+ toggle.focus();
5312
+ return;
5313
+ }
5314
+ if (e.key === "ArrowDown") {
5315
+ e.preventDefault();
5316
+ const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
5317
+ items[nextIndex].focus();
5318
+ return;
5319
+ }
5320
+ if (e.key === "ArrowUp") {
5321
+ e.preventDefault();
5322
+ const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
5323
+ items[prevIndex].focus();
5324
+ }
5325
+ },
5326
+ updateMenuSwitcher: function(switcher) {
5327
+ const toggle = switcher.querySelector(".vd-theme-switcher-toggle");
5328
+ const menu = switcher.querySelector(".vd-theme-switcher-menu");
5329
+ if (!toggle || !menu) {
5330
+ return;
5331
+ }
5332
+ const pref = this.state.preference;
5333
+ const icon = toggle.querySelector("[data-theme-icon]");
5334
+ const label = THEME_LABELS[pref] || THEME_LABELS.system;
5335
+ if (icon) {
5336
+ icon.className = THEME_ICON_CLASSES[pref] || THEME_ICON_CLASSES.system;
5337
+ }
5338
+ toggle.setAttribute("aria-label", label);
5339
+ if (toggle.hasAttribute("data-tooltip")) {
5340
+ toggle.setAttribute("data-tooltip", label);
5341
+ this.refreshTooltipContent(toggle, label);
5342
+ }
5343
+ menu.querySelectorAll("[data-theme-value]").forEach((option) => {
5344
+ const value = option.getAttribute("data-theme-value");
5345
+ const isActive = value === pref;
5346
+ option.classList.toggle("is-active", isActive);
5347
+ option.setAttribute("aria-checked", isActive ? "true" : "false");
5348
+ const tooltipText = THEME_OPTION_TOOLTIPS[value];
5349
+ if (tooltipText && option.hasAttribute("data-tooltip")) {
5350
+ option.setAttribute("data-tooltip", tooltipText);
5351
+ this.refreshTooltipContent(option, tooltipText);
5352
+ }
5353
+ });
5354
+ },
5355
+ refreshTooltipContent: function(element, text) {
5356
+ const tooltips = window.Vanduo && typeof window.Vanduo.getComponent === "function" ? window.Vanduo.getComponent("tooltips") : null;
5357
+ if (!tooltips || typeof tooltips.update !== "function") {
5358
+ return;
5359
+ }
5360
+ tooltips.update(element, text);
5361
+ },
5362
+ updateCycleToggle: function(toggle) {
5363
+ const pref = this.state.preference;
5364
+ const icon = toggle.querySelector("[data-theme-icon]");
5365
+ const label = THEME_LABELS[pref] || THEME_LABELS.system;
5366
+ if (icon) {
5367
+ icon.className = THEME_ICON_CLASSES[pref] || THEME_ICON_CLASSES.system;
5368
+ }
5369
+ toggle.setAttribute("aria-label", label);
5370
+ if (toggle.hasAttribute("data-tooltip")) {
5371
+ toggle.setAttribute("data-tooltip", label);
5372
+ this.refreshTooltipContent(toggle, label);
5373
+ }
5160
5374
  },
5161
5375
  updateUI: function(root) {
5162
5376
  const toggles = this.getToggles(root);
@@ -5168,11 +5382,26 @@
5168
5382
  if (span) {
5169
5383
  span.textContent = this.state.preference.charAt(0).toUpperCase() + this.state.preference.slice(1);
5170
5384
  }
5385
+ if (toggle.querySelector("[data-theme-icon]")) {
5386
+ this.updateCycleToggle(toggle);
5387
+ }
5171
5388
  }
5172
5389
  });
5390
+ this.getMenuSwitchers(root).forEach((switcher) => {
5391
+ this.updateMenuSwitcher(switcher);
5392
+ });
5173
5393
  },
5174
5394
  destroyAll: function(root) {
5175
5395
  const scope = root || document;
5396
+ this.getMenuSwitchers(scope).forEach((switcher) => {
5397
+ const instance = this.menuInstances.get(switcher);
5398
+ if (instance) {
5399
+ instance.cleanup.forEach((fn) => fn());
5400
+ this.closeMenu(switcher, instance.toggle, instance.menu);
5401
+ this.menuInstances.delete(switcher);
5402
+ }
5403
+ switcher.removeAttribute("data-theme-menu-initialized");
5404
+ });
5176
5405
  const toggles = this.getToggles(scope).filter(function(toggle) {
5177
5406
  return toggle.getAttribute("data-theme-initialized") === "true";
5178
5407
  });