feat: add System (auto) theme following OS prefers-color-scheme (#504)

Synthesized from PRs #506, #509, #514 (all by armorbreak001 and cloudyun888).

Implementation:
- static/index.html: flicker-prevention head script resolves 'system' to
  'dark'/'light' via matchMedia before first paint. Adds 'System (auto)'
  as first option in theme picker. onchange calls _applyTheme().
- static/boot.js: new _applyTheme(name) helper — resolves 'system' via
  matchMedia, sets data-theme, registers a MQ change listener so the UI
  tracks OS switches live. loadSettings() now calls _applyTheme() instead
  of direct data-theme assignment.
- static/commands.js: adds 'system' to valid /theme command names,
  delegates apply to _applyTheme().
- static/panels.js: _settingsThemeOnOpen reads from localStorage (preserves
  'system' string, not the resolved 'dark'/'light'). _revertSettingsPreview
  calls _applyTheme() so reverting to 'system' correctly re-enables OS tracking.
- static/i18n.js: cmd_theme description now lists 'system' first in all 5
  locales (en, es, de, zh-Hans, zh-Hant).

Design choices vs submitted PRs:
- No separate system-theme.js file (unnecessary indirection).
- matchMedia listener does NOT POST to /api/settings (OS can change rapidly;
  persisting on every OS switch would hammer the server).

Co-authored-by: armorbreak001 <armorbreak001@users.noreply.github.com>
Co-authored-by: cloudyun888 <cloudyun888@users.noreply.github.com>
This commit is contained in:
Hermes Agent
2026-04-15 07:45:20 +00:00
parent 36830e3cd1
commit 44a544362f
5 changed files with 29 additions and 12 deletions

View File

@@ -568,6 +568,21 @@ window.addEventListener('resize',()=>{
};
})();
// ── System theme helper ──────────────────────────────────────────────────────
function _applyTheme(name){
const resolved=(name==='system')
?(window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light')
:name;
document.documentElement.dataset.theme=resolved||'dark';
// Re-register OS change listener whenever system theme is active
if(name==='system'){
const mq=window.matchMedia('(prefers-color-scheme:dark)');
const _onOsChange=()=>{ document.documentElement.dataset.theme=mq.matches?'dark':'light'; };
mq.removeEventListener('change',_onOsChange);
mq.addEventListener('change',_onOsChange);
}
}
function applyBotName(){
const name=window._botName||'Hermes';
document.title=name;
@@ -594,8 +609,8 @@ function applyBotName(){
window._notificationsEnabled=!!s.notifications_enabled;
window._botName=s.bot_name||'Hermes';
const _theme=s.theme||'dark';
document.documentElement.dataset.theme=_theme;
localStorage.setItem('hermes-theme',_theme);
_applyTheme(_theme);
document.body.classList.toggle('bubble-layout',!!s.bubble_layout);
if(typeof setLocale==='function'){
const _lang=typeof resolvePreferredLocale==='function'