Skip to content

Commit fa65a88

Browse files
workerCaFeZn
authored andcommitted
Refine onboarding theme toggle
1 parent 8984110 commit fa65a88

2 files changed

Lines changed: 117 additions & 35 deletions

File tree

static/XRobot-Onboarding/index.html

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,10 @@
9494

9595
.theme-toggle {
9696
display: inline-flex;
97-
gap: 6px;
98-
padding: 6px;
97+
align-items: center;
98+
justify-content: center;
99+
width: 44px;
100+
height: 44px;
99101
border-radius: 999px;
100102
background: var(--toggle-bg);
101103
border: 1px solid var(--toggle-border);
@@ -105,24 +107,57 @@
105107
}
106108

107109
.theme-toggle button {
110+
width: 100%;
111+
height: 100%;
108112
border: none;
109113
background: transparent;
110114
color: var(--toggle-button-text);
111-
font-size: 13px;
112-
font-weight: 600;
113115
border-radius: 999px;
114-
padding: 8px 14px;
116+
padding: 0;
115117
cursor: pointer;
118+
display: inline-flex;
119+
align-items: center;
120+
justify-content: center;
116121
transition: background 0.15s ease, color 0.15s ease, transform 0.05s ease;
117122
}
118123

119124
.theme-toggle button:hover {
120125
transform: translateY(-1px);
121126
}
122127

123-
.theme-toggle button.active {
124-
background: var(--toggle-button-active-bg);
125-
color: var(--toggle-button-active-text);
128+
.theme-toggle button:focus-visible {
129+
outline: 2px solid var(--link-color);
130+
outline-offset: 2px;
131+
}
132+
133+
.theme-toggle-icon {
134+
width: 20px;
135+
height: 20px;
136+
display: block;
137+
}
138+
139+
.theme-toggle-icon--moon {
140+
display: none;
141+
}
142+
143+
html[data-theme='dark'] .theme-toggle-icon--sun {
144+
display: none;
145+
}
146+
147+
html[data-theme='dark'] .theme-toggle-icon--moon {
148+
display: block;
149+
}
150+
151+
.sr-only {
152+
position: absolute;
153+
width: 1px;
154+
height: 1px;
155+
padding: 0;
156+
margin: -1px;
157+
overflow: hidden;
158+
clip: rect(0, 0, 0, 0);
159+
white-space: nowrap;
160+
border: 0;
126161
}
127162

128163
.card {
@@ -330,12 +365,7 @@
330365
}
331366

332367
.theme-toggle {
333-
width: 100%;
334-
justify-content: center;
335-
}
336-
337-
.theme-toggle button {
338-
flex: 1;
368+
align-self: flex-end;
339369
}
340370
}
341371
</style>
@@ -350,9 +380,16 @@ <h1 id="page-title">学习路线引导</h1>
350380
</div>
351381
</div>
352382

353-
<div class="theme-toggle" role="group" aria-label="切换深浅色模式">
354-
<button type="button" data-theme-option="light">浅色</button>
355-
<button type="button" data-theme-option="dark">深色</button>
383+
<div class="theme-toggle">
384+
<button type="button" id="theme-toggle-button" aria-label="切换到深色模式" title="切换到深色模式">
385+
<span class="sr-only" id="theme-toggle-text">切换到深色模式</span>
386+
<svg class="theme-toggle-icon theme-toggle-icon--sun" viewBox="0 0 24 24" aria-hidden="true">
387+
<path fill="currentColor" d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12Zm0-16a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1Zm0 18a1 1 0 0 1 1 1v1a1 1 0 1 1-2 0v-1a1 1 0 0 1 1-1Zm10-9a1 1 0 1 1 0 2h-2a1 1 0 1 1 0-2h2ZM4 12a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2h2Zm15.07 6.66a1 1 0 0 1 1.41 1.41l-.7.71a1 1 0 0 1-1.42-1.41l.71-.71ZM5.64 5.64a1 1 0 0 1 1.41 0 1 1 0 0 1 0 1.41l-.71.71A1 1 0 0 1 4.93 6.35l.71-.71Zm13.43 0 .71.71a1 1 0 1 1-1.41 1.41l-.71-.71a1 1 0 0 1 1.41-1.41ZM6.34 18.36l.71.71a1 1 0 0 1-1.41 1.41l-.71-.71a1 1 0 0 1 1.41-1.41Z"/>
388+
</svg>
389+
<svg class="theme-toggle-icon theme-toggle-icon--moon" viewBox="0 0 24 24" aria-hidden="true">
390+
<path fill="currentColor" d="M21.75 15.5A9 9 0 1 1 12.5 2.25a1 1 0 0 1 .9 1.55A7 7 0 0 0 20.2 14.6a1 1 0 0 1 1.55.9Z"/>
391+
</svg>
392+
</button>
356393
</div>
357394
</div>
358395

static/XRobot-Onboarding/onboarding.js

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
const CONFIG_URL = "config.json";
44
const THEME_STORAGE_KEY = "xrobot_onboarding_theme";
5+
const DARK_MODE_MEDIA_QUERY = "(prefers-color-scheme: dark)";
56

67
let treeConfig = null;
78
let state = null;
9+
let themeMediaQuery = null;
810

911
const mdTextCache = {};
1012

@@ -16,7 +18,7 @@ function md(text) {
1618
return String(text);
1719
}
1820

19-
function getPreferredTheme() {
21+
function getStoredThemePreference() {
2022
try {
2123
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
2224
if (savedTheme === "light" || savedTheme === "dark") {
@@ -26,38 +28,81 @@ function getPreferredTheme() {
2628
console.warn("读取主题偏好失败", e);
2729
}
2830

29-
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
31+
return null;
32+
}
33+
34+
function getSystemTheme() {
35+
if (window.matchMedia && window.matchMedia(DARK_MODE_MEDIA_QUERY).matches) {
3036
return "dark";
3137
}
3238

3339
return "light";
3440
}
3541

36-
function syncThemeToggleButtons(theme) {
37-
document.querySelectorAll("[data-theme-option]").forEach(btn => {
38-
btn.classList.toggle("active", btn.dataset.themeOption === theme);
39-
});
42+
function getPreferredTheme() {
43+
return getStoredThemePreference() || getSystemTheme();
4044
}
4145

42-
function applyTheme(theme) {
46+
function updateThemeToggleLabel(theme) {
47+
const nextTheme = theme === "dark" ? "light" : "dark";
48+
const nextLabel = nextTheme === "dark" ? "切换到深色模式" : "切换到浅色模式";
49+
const toggleButton = document.getElementById("theme-toggle-button");
50+
const toggleText = document.getElementById("theme-toggle-text");
51+
52+
if (toggleButton) {
53+
toggleButton.setAttribute("aria-label", nextLabel);
54+
toggleButton.setAttribute("title", nextLabel);
55+
}
56+
57+
if (toggleText) {
58+
toggleText.textContent = nextLabel;
59+
}
60+
}
61+
62+
function applyTheme(theme, options = {}) {
4363
const nextTheme = theme === "dark" ? "dark" : "light";
64+
const { persist = true } = options;
4465
document.documentElement.setAttribute("data-theme", nextTheme);
45-
syncThemeToggleButtons(nextTheme);
66+
updateThemeToggleLabel(nextTheme);
4667

47-
try {
48-
localStorage.setItem(THEME_STORAGE_KEY, nextTheme);
49-
} catch (e) {
50-
console.warn("保存主题偏好失败", e);
68+
if (persist) {
69+
try {
70+
localStorage.setItem(THEME_STORAGE_KEY, nextTheme);
71+
} catch (e) {
72+
console.warn("保存主题偏好失败", e);
73+
}
74+
}
75+
}
76+
77+
function handleSystemThemeChange(event) {
78+
if (getStoredThemePreference()) {
79+
return;
5180
}
81+
82+
applyTheme(event.matches ? "dark" : "light", { persist: false });
5283
}
5384

5485
function initThemeToggle() {
55-
const theme = getPreferredTheme();
56-
syncThemeToggleButtons(theme);
57-
document.querySelectorAll("[data-theme-option]").forEach(btn => {
58-
btn.addEventListener("click", () => applyTheme(btn.dataset.themeOption));
59-
});
60-
applyTheme(theme);
86+
const toggleButton = document.getElementById("theme-toggle-button");
87+
const storedTheme = getStoredThemePreference();
88+
applyTheme(storedTheme || getSystemTheme(), { persist: !!storedTheme });
89+
90+
if (toggleButton) {
91+
toggleButton.addEventListener("click", () => {
92+
const currentTheme = document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light";
93+
const nextTheme = currentTheme === "dark" ? "light" : "dark";
94+
applyTheme(nextTheme, { persist: true });
95+
});
96+
}
97+
98+
if (window.matchMedia) {
99+
themeMediaQuery = window.matchMedia(DARK_MODE_MEDIA_QUERY);
100+
if (typeof themeMediaQuery.addEventListener === "function") {
101+
themeMediaQuery.addEventListener("change", handleSystemThemeChange);
102+
} else if (typeof themeMediaQuery.addListener === "function") {
103+
themeMediaQuery.addListener(handleSystemThemeChange);
104+
}
105+
}
61106
}
62107

63108
function loadMdIntoElement(path, el) {

0 commit comments

Comments
 (0)