feat: system theme option + fix toast timer overlap
Lint & Test / test (push) Successful in 1m27s

Add third theme mode (system) that follows OS prefers-color-scheme.
Theme button cycles dark → light → system with monitor icon.
Listens for OS preference changes in real time when in system mode.

Fix showToast clearing previous timer so rapid calls don't cause
the toast to disappear early.
This commit is contained in:
2026-03-30 13:55:38 +03:00
parent 4b7a8d75f4
commit db5008aaeb
5 changed files with 174 additions and 149 deletions
+42 -18
View File
@@ -279,30 +279,54 @@
if (btn) btn.style.opacity = state === 'on' ? '1' : '0.5';
}
// Initialize theme
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
// Initialize theme (preference can be 'dark', 'light', or 'system')
const _systemDarkMq = window.matchMedia('(prefers-color-scheme: dark)');
function updateThemeIcon(theme) {
function _resolveTheme(pref) {
if (pref === 'system') return _systemDarkMq.matches ? 'dark' : 'light';
return pref;
}
function _applyTheme(resolved) {
document.documentElement.setAttribute('data-theme', resolved);
if (window._updateBgAnimTheme) window._updateBgAnimTheme(resolved === 'dark');
const accent = localStorage.getItem('accentColor');
if (accent) applyAccentColor(accent, true);
}
const _themePref = localStorage.getItem('theme') || 'dark';
_applyTheme(_resolveTheme(_themePref));
updateThemeIcon(_themePref);
// Listen for OS preference changes when in system mode
_systemDarkMq.addEventListener('change', function() {
if (localStorage.getItem('theme') === 'system') {
_applyTheme(_resolveTheme('system'));
}
});
function updateThemeIcon(pref) {
const icon = document.getElementById('theme-icon');
icon.innerHTML = theme === 'dark'
? '<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>'
: '<svg class="icon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>';
if (pref === 'system') {
icon.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>';
} else if (pref === 'dark') {
icon.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>';
} else {
icon.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>';
}
}
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
const current = localStorage.getItem('theme') || 'dark';
const order = ['dark', 'light', 'system'];
const next = order[(order.indexOf(current) + 1) % order.length];
const resolved = _resolveTheme(next);
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
if (window._updateBgAnimTheme) window._updateBgAnimTheme(newTheme === 'dark');
// Re-derive accent text variant for the new theme
const accent = localStorage.getItem('accentColor');
if (accent) applyAccentColor(accent, true);
showToast(window.t ? t(newTheme === 'dark' ? 'theme.switched.dark' : 'theme.switched.light') : `Switched to ${newTheme} theme`, 'info');
localStorage.setItem('theme', next);
_applyTheme(resolved);
updateThemeIcon(next);
const toastKeys = { dark: 'theme.switched.dark', light: 'theme.switched.light', system: 'theme.switched.system' };
showToast(window.t ? t(toastKeys[next]) : `Switched to ${next} theme`, 'info');
}
// Initialize accent color