@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 +15 -3
- package/css/components/theme-switcher.css +129 -0
- package/css/vanduo.css +1 -0
- package/dist/build-info.json +3 -3
- package/dist/vanduo.cjs.js +244 -15
- package/dist/vanduo.cjs.js.map +2 -2
- package/dist/vanduo.cjs.min.js +6 -6
- package/dist/vanduo.cjs.min.js.map +3 -3
- package/dist/vanduo.css +117 -1
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +244 -15
- package/dist/vanduo.esm.js.map +2 -2
- package/dist/vanduo.esm.min.js +6 -6
- package/dist/vanduo.esm.min.js.map +3 -3
- package/dist/vanduo.js +244 -15
- package/dist/vanduo.js.map +2 -2
- package/dist/vanduo.min.css +2 -2
- package/dist/vanduo.min.css.map +1 -1
- package/dist/vanduo.min.js +6 -6
- package/dist/vanduo.min.js.map +3 -3
- package/js/components/code-snippet.js +14 -8
- package/js/components/theme-switcher.js +290 -29
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Vanduo Framework v1.4.
|
|
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.
|
|
33
|
-
<script src="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.4.
|
|
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');
|
package/dist/build-info.json
CHANGED
package/dist/vanduo.cjs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Vanduo v1.4.
|
|
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.
|
|
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 (
|
|
490
|
-
|
|
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
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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(
|
|
5109
|
+
return window.Vanduo.queryAll(scope, '.vd-theme-switcher[data-theme-ui="menu"]');
|
|
5080
5110
|
}
|
|
5081
|
-
return Array.from(
|
|
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 =
|
|
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
|
});
|