feat: even better theme switcher

This commit is contained in:
grassblock 2025-05-03 20:51:35 +08:00
parent 1da8ae56c4
commit c6db8689b4
2 changed files with 166 additions and 50 deletions

View file

@ -1,42 +1,119 @@
--- ---
--- ---
<button class="theme-switcher" id="theme-switcher"> <div class="theme-dropdown" id="theme-dropdown">
Toggle Theme <button class="theme-switcher" id="theme-switcher">Theme</button>
</button> <div class="menu-body" id="menu-body">
<div class="dropdown-item" data-theme="auto">System</div>
<div class="dropdown-item" data-theme="dark">Dark</div>
<div class="dropdown-item" data-theme="light">Light</div>
</div>
</div>
<script> <style>
// Inspired by https://blog.skk.moe/post/hello-darkmode-my-old-friend/ .theme-dropdown {
const themeSwitcher = document.getElementById('theme-switcher'); position: relative;
const root = document.documentElement; display: inline-block;
const preferredTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? 'dark' : 'light'
// Check for saved theme preference or default to dark
const savedTheme = localStorage.getItem('theme');
// If the saved theme is the same as the system preferred theme, set it and remove it from localStorage
if (savedTheme === preferredTheme) {
root.removeAttribute('data-theme');
localStorage.removeItem('theme');
} else if (savedTheme) {
// Or if another saved theme is found, set it
root.setAttribute('data-theme', savedTheme);
} else {
// Or if no saved theme is found, use the system preferred theme until the theme is toggled
root.removeAttribute('data-theme');
localStorage.removeItem('theme');
} }
themeSwitcher?.addEventListener('click', () => { .menu-body {
// get current theme from localStorage display: none;
const currentTheme = localStorage.getItem('theme'); position: fixed;
// check if the current theme is correctly set, toggles it and saves it to localStorage right: 45px;
if (currentTheme !== null && ['dark', 'light'].includes(currentTheme)) { bottom: 70px;
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; background-color: var(--bg-color);
root.setAttribute('data-theme', newTheme); border: 1px solid var(--border-color);
localStorage.setItem('theme', newTheme); min-width: 140px;
z-index: 20;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.dropdown-item {
padding: 0.5rem 1rem;
cursor: pointer;
transition: background-color 0.2s ease;
}
.dropdown-item:hover {
background-color: var(--border-color);
}
.dropdown-item.active {
color: var(--accent-color);
font-weight: bold;
}
.theme-dropdown:hover .menu-body {
display: block;
}
</style>
<script>
// Constants for theme options
const THEME_AUTO = 'auto';
const THEME_DARK = 'dark';
const THEME_LIGHT = 'light';
// DOM elements
const themeDropdown = document.getElementById('theme-dropdown');
const menuBody = document.getElementById('menu-body');
const dropdownItems = document.querySelectorAll('.dropdown-item');
const root = document.documentElement;
// Get system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const systemTheme = prefersDark ? THEME_DARK : THEME_LIGHT;
// Get saved theme from localStorage or default to auto
const savedThemeMode = localStorage.getItem('themeMode') || THEME_AUTO;
const savedTheme = localStorage.getItem('theme');
// Function to update active state in dropdown menu
function updateActiveState(activeTheme) {
dropdownItems.forEach(item => {
if (item.getAttribute('data-theme') === activeTheme) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
// Function to apply theme
function applyTheme(themeMode) {
// Update active state in dropdown
updateActiveState(themeMode);
// Save theme mode preference
localStorage.setItem('themeMode', themeMode);
// Apply the appropriate theme
if (themeMode === THEME_AUTO) {
// Follow system
localStorage.removeItem('theme');
root.removeAttribute('data-theme');
} else { } else {
// if not set, toggle from the system preferred theme and save it to localStorage // Set to specific theme
const newTheme = preferredTheme === 'dark' ? 'light' : 'dark'; root.setAttribute('data-theme', themeMode);
root.setAttribute('data-theme', newTheme); localStorage.setItem('theme', themeMode);
localStorage.setItem('theme', newTheme); }
}
// Initialize theme
applyTheme(savedThemeMode);
// Add click handlers to dropdown items
dropdownItems.forEach(item => {
item.addEventListener('click', () => {
const selectedTheme = item.getAttribute('data-theme');
applyTheme(selectedTheme);
});
});
// Listen for system preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (savedThemeMode === THEME_AUTO) {
// Only update if we're in auto mode
root.removeAttribute('data-theme');
} }
}); });
</script> </script>

View file

@ -143,6 +143,51 @@ main {
vertical-align: middle; vertical-align: middle;
} }
.footer .floating {
position: fixed;
bottom: 20px;
right: 30px;
z-index: 10;
display: flex;
flex-direction: column;
border: none;
outline: none;
padding: 15px;
}
#toTopBtn {
display: none;
background: var(--border-color);
border: none;
color: var(--text-color);
padding: 0.5rem 1rem;
font-family: var(--font-mono);
cursor: pointer;
}
#toTopBtn:hover {
background: var(--accent-color);
color: var(--bg-color);
}
/* Theme Switcher */
.theme-switcher {
background: var(--border-color);
border: none;
color: var(--text-color);
padding: 0.5rem 1rem;
font-family: var(--font-mono);
cursor: pointer;
transition: background-color 0.3s ease, color 0.3s ease;
}
.theme-switcher:hover {
background: var(--accent-color);
color: var(--bg-color);
}
/* Post styles */ /* Post styles */
.post-title { .post-title {
color: var(--header-color); color: var(--header-color);
@ -173,24 +218,18 @@ main {
opacity: 0.7; opacity: 0.7;
} }
/* Theme Switcher */ /* helper Class */
.theme-switcher { .fade-in {
position: fixed; opacity: 1;
top: 1rem; transition-property: opacity;
right: 1rem; transition-duration: .7s;
background: var(--border-color); transition-timing-function: cubic-bezier(.4,0,1,1);
border: none;
color: var(--text-color);
padding: 0.5rem 1rem;
border-radius: 4px;
font-family: var(--font-mono);
cursor: pointer;
transition: background-color 0.3s ease, color 0.3s ease;
} }
.fade-out {
.theme-switcher:hover { opacity: 0;
background: var(--accent-color); transition-property: opacity;
color: var(--bg-color); transition-duration: .7s;
transition-timing-function: cubic-bezier(.4,0,1,1);
} }
/* Media Queries */ /* Media Queries */