feat(theme): replace color scheme system with light/dark + accent skins (PR #627 by @aronprins)
Independent review by @nesquena confirmed all blockers resolved. Theme×skin two-axis system replaces old monolithic color schemes. Closes #627. Co-Authored-By: aronprins <aronprins@users.noreply.github.com>
This commit is contained in:
159
static/boot.js
159
static/boot.js
@@ -579,29 +579,134 @@ window.addEventListener('resize',()=>{
|
||||
};
|
||||
})();
|
||||
|
||||
// ── System theme helper ──────────────────────────────────────────────────────
|
||||
// ── Appearance helpers (theme = light/dark/system, skin = accent color) ──────
|
||||
const _SKINS=[
|
||||
{name:'Default', colors:['#FFD700','#FFBF00','#CD7F32']},
|
||||
{name:'Ares', colors:['#FF4444','#CC3333','#992222']},
|
||||
{name:'Mono', colors:['#CCCCCC','#999999','#666666']},
|
||||
{name:'Slate', colors:['#334155','#475569','#64748b']},
|
||||
{name:'Poseidon', colors:['#0EA5E9','#0284C7','#0369A1']},
|
||||
{name:'Sisyphus', colors:['#A78BFA','#8B5CF6','#7C3AED']},
|
||||
{name:'Charizard',colors:['#FB923C','#F97316','#EA580C']},
|
||||
];
|
||||
const _VALID_THEMES=new Set(['system','dark','light']);
|
||||
const _VALID_SKINS=new Set((_SKINS||[]).map(s=>s.name.toLowerCase()));
|
||||
const _LEGACY_THEME_MAP={
|
||||
slate:{theme:'dark',skin:'slate'},
|
||||
solarized:{theme:'dark',skin:'poseidon'},
|
||||
monokai:{theme:'dark',skin:'sisyphus'},
|
||||
nord:{theme:'dark',skin:'slate'},
|
||||
oled:{theme:'dark',skin:'default'},
|
||||
};
|
||||
let _systemThemeMq=null;
|
||||
let _onSystemThemeChange=null;
|
||||
|
||||
function _normalizeAppearance(theme,skin){
|
||||
const rawTheme=typeof theme==='string'?theme.trim().toLowerCase():'';
|
||||
const rawSkin=typeof skin==='string'?skin.trim().toLowerCase():'';
|
||||
const legacy=_LEGACY_THEME_MAP[rawTheme];
|
||||
const nextTheme=legacy?legacy.theme:(_VALID_THEMES.has(rawTheme)?rawTheme:'dark');
|
||||
const nextSkin=_VALID_SKINS.has(rawSkin)?rawSkin:(legacy?legacy.skin:'default');
|
||||
return {theme:nextTheme,skin:nextSkin};
|
||||
}
|
||||
|
||||
function _setResolvedTheme(isDark){
|
||||
document.documentElement.classList.toggle('dark',!!isDark);
|
||||
const link=document.getElementById('prism-theme');
|
||||
if(!link) return;
|
||||
const want=isDark
|
||||
?'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css'
|
||||
:'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css';
|
||||
if(link.href!==want){ link.href=want; }
|
||||
}
|
||||
|
||||
function _applyTheme(name){
|
||||
const resolved=(name==='system')
|
||||
?(window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light')
|
||||
:name;
|
||||
document.documentElement.dataset.theme=resolved||'dark';
|
||||
// Swap Prism syntax-highlighting theme to match UI theme
|
||||
(function(){
|
||||
const link=document.getElementById('prism-theme');
|
||||
if(!link) return;
|
||||
const isDark=(resolved!=='light');
|
||||
const want=isDark
|
||||
?'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css'
|
||||
:'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css';
|
||||
if(link.href!==want){ link.href=want; }
|
||||
})();
|
||||
// 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);
|
||||
const normalized=_normalizeAppearance(name,'default');
|
||||
if(_systemThemeMq&&_onSystemThemeChange){
|
||||
_systemThemeMq.removeEventListener('change',_onSystemThemeChange);
|
||||
_systemThemeMq=null;
|
||||
_onSystemThemeChange=null;
|
||||
}
|
||||
if(normalized.theme==='system'){
|
||||
_systemThemeMq=window.matchMedia('(prefers-color-scheme:dark)');
|
||||
_onSystemThemeChange=()=>_setResolvedTheme(_systemThemeMq.matches);
|
||||
_setResolvedTheme(_systemThemeMq.matches);
|
||||
_systemThemeMq.addEventListener('change',_onSystemThemeChange);
|
||||
return;
|
||||
}
|
||||
_setResolvedTheme(normalized.theme==='dark');
|
||||
}
|
||||
|
||||
function _applySkin(name){
|
||||
const key=(name||'default').toLowerCase();
|
||||
if(key==='default') delete document.documentElement.dataset.skin;
|
||||
else document.documentElement.dataset.skin=key;
|
||||
}
|
||||
|
||||
function _pickTheme(name){
|
||||
const currentSkin=localStorage.getItem('hermes-skin');
|
||||
const appearance=_normalizeAppearance(name,currentSkin);
|
||||
localStorage.setItem('hermes-theme',appearance.theme);
|
||||
localStorage.setItem('hermes-skin',appearance.skin);
|
||||
_applyTheme(appearance.theme);
|
||||
_applySkin(appearance.skin);
|
||||
_syncThemePicker(appearance.theme);
|
||||
_syncSkinPicker(appearance.skin);
|
||||
if(typeof _markSettingsDirty==='function') _markSettingsDirty();
|
||||
const hidden=$('settingsTheme');
|
||||
if(hidden) hidden.value=appearance.theme;
|
||||
const skinHidden=$('settingsSkin');
|
||||
if(skinHidden) skinHidden.value=appearance.skin;
|
||||
}
|
||||
|
||||
function _pickSkin(name){
|
||||
const appearance=_normalizeAppearance(localStorage.getItem('hermes-theme'),name);
|
||||
localStorage.setItem('hermes-theme',appearance.theme);
|
||||
localStorage.setItem('hermes-skin',appearance.skin);
|
||||
_applyTheme(appearance.theme);
|
||||
_applySkin(appearance.skin);
|
||||
_syncThemePicker(appearance.theme);
|
||||
_syncSkinPicker(appearance.skin);
|
||||
if(typeof _markSettingsDirty==='function') _markSettingsDirty();
|
||||
const hidden=$('settingsSkin');
|
||||
if(hidden) hidden.value=appearance.skin;
|
||||
const themeHidden=$('settingsTheme');
|
||||
if(themeHidden) themeHidden.value=appearance.theme;
|
||||
}
|
||||
|
||||
function _syncThemePicker(active){
|
||||
document.querySelectorAll('#themePickerGrid .theme-pick-btn').forEach(btn=>{
|
||||
const sel=btn.dataset.themeVal===active;
|
||||
btn.style.borderColor=sel?'var(--accent)':'var(--border2)';
|
||||
btn.style.boxShadow=sel?'0 0 0 1px var(--accent-bg-strong)':'none';
|
||||
});
|
||||
}
|
||||
|
||||
function _syncSkinPicker(active){
|
||||
document.querySelectorAll('#skinPickerGrid .skin-pick-btn').forEach(btn=>{
|
||||
const sel=btn.dataset.skinVal===active;
|
||||
btn.style.borderColor=sel?'var(--accent)':'var(--border2)';
|
||||
btn.style.boxShadow=sel?'0 0 0 1px var(--accent-bg-strong)':'none';
|
||||
});
|
||||
}
|
||||
|
||||
function _buildSkinPicker(activeSkin){
|
||||
const grid=$('skinPickerGrid');
|
||||
if(!grid) return;
|
||||
grid.innerHTML='';
|
||||
for(const skin of _SKINS){
|
||||
const key=skin.name.toLowerCase();
|
||||
const btn=document.createElement('button');
|
||||
btn.type='button';
|
||||
btn.className='skin-pick-btn';
|
||||
btn.dataset.skinVal=key;
|
||||
btn.style.cssText='border:1px solid var(--border2);border-radius:8px;padding:8px 4px;text-align:center;cursor:pointer;background:none;transition:all .15s';
|
||||
btn.onclick=()=>_pickSkin(skin.name);
|
||||
const dots=skin.colors.map(c=>`<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${c}"></span>`).join('');
|
||||
btn.innerHTML=`<div style="display:flex;gap:3px;justify-content:center;margin-bottom:4px">${dots}</div><span style="font-size:11px;color:var(--text)">${skin.name}</span>`;
|
||||
grid.appendChild(btn);
|
||||
}
|
||||
_syncSkinPicker((activeSkin||'default').toLowerCase());
|
||||
}
|
||||
|
||||
function applyBotName(){
|
||||
@@ -629,9 +734,11 @@ function applyBotName(){
|
||||
window._soundEnabled=!!s.sound_enabled;
|
||||
window._notificationsEnabled=!!s.notifications_enabled;
|
||||
window._botName=s.bot_name||'Hermes';
|
||||
const _theme=s.theme||'dark';
|
||||
localStorage.setItem('hermes-theme',_theme);
|
||||
_applyTheme(_theme);
|
||||
const appearance=_normalizeAppearance(s.theme,s.skin);
|
||||
localStorage.setItem('hermes-theme',appearance.theme);
|
||||
_applyTheme(appearance.theme);
|
||||
localStorage.setItem('hermes-skin',appearance.skin);
|
||||
_applySkin(appearance.skin);
|
||||
document.body.classList.toggle('bubble-layout',!!s.bubble_layout);
|
||||
if(typeof setLocale==='function'){
|
||||
const _lang=typeof resolvePreferredLocale==='function'
|
||||
@@ -695,10 +802,12 @@ function applyBotName(){
|
||||
if(S.session&&S.session.workspace&&localStorage.getItem('hermes-webui-workspace-panel')==='open'){
|
||||
_workspacePanelMode='browse';
|
||||
}
|
||||
syncWorkspacePanelState();await renderSessionList();if(typeof startGatewaySSE==='function')startGatewaySSE();await checkInflightOnBoot(saved);return;}
|
||||
S._bootReady=true;
|
||||
syncTopbar();syncWorkspacePanelState();await renderSessionList();if(typeof startGatewaySSE==='function')startGatewaySSE();await checkInflightOnBoot(saved);return;}
|
||||
catch(e){localStorage.removeItem('hermes-webui-session');}
|
||||
}
|
||||
// no saved session - show empty state, wait for user to hit +
|
||||
S._bootReady=true;
|
||||
syncTopbar();
|
||||
syncWorkspacePanelState();
|
||||
$('emptyState').style.display='';
|
||||
|
||||
@@ -121,19 +121,48 @@ async function cmdUsage(){
|
||||
}
|
||||
|
||||
async function cmdTheme(args){
|
||||
const themes=['system','dark','light','slate','solarized','monokai','nord','oled'];
|
||||
if(!args||!themes.includes(args.toLowerCase())){
|
||||
showToast(t('theme_usage')+themes.join('|'));
|
||||
const themes=['system','dark','light'];
|
||||
const skins=(_SKINS||[]).map(s=>s.name.toLowerCase());
|
||||
const legacyThemes=Object.keys(_LEGACY_THEME_MAP||{});
|
||||
const val=(args||'').toLowerCase().trim();
|
||||
// Check if it's a theme
|
||||
if(themes.includes(val)||legacyThemes.includes(val)){
|
||||
const appearance=_normalizeAppearance(
|
||||
val,
|
||||
legacyThemes.includes(val)?null:localStorage.getItem('hermes-skin')
|
||||
);
|
||||
localStorage.setItem('hermes-theme',appearance.theme);
|
||||
localStorage.setItem('hermes-skin',appearance.skin);
|
||||
_applyTheme(appearance.theme);
|
||||
_applySkin(appearance.skin);
|
||||
try{await api('/api/settings',{method:'POST',body:JSON.stringify({theme:appearance.theme,skin:appearance.skin})});}catch(e){}
|
||||
const sel=$('settingsTheme');
|
||||
if(sel)sel.value=appearance.theme;
|
||||
const skinSel=$('settingsSkin');
|
||||
if(skinSel)skinSel.value=appearance.skin;
|
||||
if(typeof _syncThemePicker==='function') _syncThemePicker(appearance.theme);
|
||||
if(typeof _syncSkinPicker==='function') _syncSkinPicker(appearance.skin);
|
||||
showToast(t('theme_set')+appearance.theme+(legacyThemes.includes(val)?` + ${appearance.skin}`:''));
|
||||
return;
|
||||
}
|
||||
const themeName=args.toLowerCase();
|
||||
localStorage.setItem('hermes-theme',themeName);
|
||||
_applyTheme(themeName);
|
||||
try{await api('/api/settings',{method:'POST',body:JSON.stringify({theme:themeName})});}catch(e){}
|
||||
// Update settings dropdown if panel is open
|
||||
const sel=$('settingsTheme');
|
||||
if(sel)sel.value=themeName;
|
||||
showToast(t('theme_set')+themeName);
|
||||
// Check if it's a skin
|
||||
if(skins.includes(val)){
|
||||
const appearance=_normalizeAppearance(localStorage.getItem('hermes-theme'),val);
|
||||
localStorage.setItem('hermes-theme',appearance.theme);
|
||||
localStorage.setItem('hermes-skin',appearance.skin);
|
||||
_applyTheme(appearance.theme);
|
||||
_applySkin(appearance.skin);
|
||||
try{await api('/api/settings',{method:'POST',body:JSON.stringify({theme:appearance.theme,skin:appearance.skin})});}catch(e){}
|
||||
const sel=$('settingsSkin');
|
||||
if(sel)sel.value=appearance.skin;
|
||||
const themeSel=$('settingsTheme');
|
||||
if(themeSel)themeSel.value=appearance.theme;
|
||||
if(typeof _syncThemePicker==='function') _syncThemePicker(appearance.theme);
|
||||
if(typeof _syncSkinPicker==='function') _syncSkinPicker(appearance.skin);
|
||||
showToast(t('theme_set')+appearance.skin);
|
||||
return;
|
||||
}
|
||||
showToast(t('theme_usage')+themes.join('|')+' | '+skins.join('|')+' | legacy:'+legacyThemes.join('|'));
|
||||
}
|
||||
|
||||
async function cmdSkills(args){
|
||||
|
||||
@@ -66,7 +66,7 @@ const LOCALES = {
|
||||
cmd_workspace: 'Switch workspace by name',
|
||||
cmd_new: 'Start a new chat session',
|
||||
cmd_usage: 'Toggle token usage display on/off',
|
||||
cmd_theme: 'Switch theme (system/dark/light/slate/solarized/monokai/nord/oled)',
|
||||
cmd_theme: 'Switch appearance (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard)',
|
||||
cmd_personality: 'Switch agent personality',
|
||||
cmd_skills: 'List available Hermes skills',
|
||||
available_commands: 'Available commands:',
|
||||
@@ -137,6 +137,7 @@ const LOCALES = {
|
||||
settings_label_model: 'Default Model',
|
||||
settings_label_send_key: 'Send Key',
|
||||
settings_label_theme: 'Theme',
|
||||
settings_label_skin: 'Skin',
|
||||
settings_label_language: 'Language',
|
||||
settings_label_token_usage: 'Show token usage',
|
||||
settings_label_bubble_layout: 'Chat bubble layout',
|
||||
@@ -480,7 +481,7 @@ const LOCALES = {
|
||||
cmd_workspace: 'Cambiar de espacio de trabajo por nombre',
|
||||
cmd_new: 'Iniciar una nueva sesión de chat',
|
||||
cmd_usage: 'Activar o desactivar el uso de tokens',
|
||||
cmd_theme: 'Cambiar tema (system/dark/light/slate/solarized/monokai/nord/oled)',
|
||||
cmd_theme: 'Cambiar apariencia (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard)',
|
||||
cmd_personality: 'Cambiar la personalidad del agente',
|
||||
cmd_skills: 'Listar las skills de Hermes disponibles',
|
||||
available_commands: 'Comandos disponibles:',
|
||||
@@ -543,6 +544,7 @@ const LOCALES = {
|
||||
settings_label_model: 'Modelo predeterminado',
|
||||
settings_label_send_key: 'Tecla de envío',
|
||||
settings_label_theme: 'Tema',
|
||||
settings_label_skin: 'Piel',
|
||||
settings_label_language: 'Idioma',
|
||||
settings_label_token_usage: 'Mostrar uso de tokens',
|
||||
settings_label_bubble_layout: 'Disposición en burbujas',
|
||||
@@ -884,7 +886,7 @@ const LOCALES = {
|
||||
cmd_workspace: 'Workspace nach Namen wechseln',
|
||||
cmd_new: 'Neue Chat-Sitzung starten',
|
||||
cmd_usage: 'Token-Verbrauchsanzeige umschalten',
|
||||
cmd_theme: 'Theme wechseln (system/dark/light/slate/solarized/monokai/nord/oled)',
|
||||
cmd_theme: 'Darstellung wechseln (Theme: system/dark/light, Skin: default/ares/mono/slate/poseidon/sisyphus/charizard)',
|
||||
cmd_personality: 'Agenten-Persönlichkeit wechseln',
|
||||
cmd_skills: 'Verfügbare Hermes-Skills auflisten',
|
||||
available_commands: 'Verfügbare Befehle:',
|
||||
@@ -955,6 +957,7 @@ const LOCALES = {
|
||||
settings_label_model: 'Standard-Modell',
|
||||
settings_label_send_key: 'Sende-Taste',
|
||||
settings_label_theme: 'Theme',
|
||||
settings_label_skin: 'Skin',
|
||||
settings_label_language: 'Sprache',
|
||||
settings_label_token_usage: 'Token-Verbrauch anzeigen',
|
||||
settings_label_cli_sessions: 'Agent-Sitzungen anzeigen',
|
||||
@@ -1096,7 +1099,7 @@ const LOCALES = {
|
||||
cmd_workspace: '\u6309\u540d\u79f0\u5207\u6362\u5de5\u4f5c\u533a',
|
||||
cmd_new: '\u65b0\u5efa\u804a\u5929\u4f1a\u8bdd',
|
||||
cmd_usage: '\u5207\u6362 token \u7528\u91cf\u663e\u793a',
|
||||
cmd_theme: '\u5207\u6362\u4e3b\u9898\uff08system/dark/light/slate/solarized/monokai/nord/oled\uff09',
|
||||
cmd_theme: '\u5207\u6362\u5916\u89c2\uff08\u4e3b\u9898\uff1asystem/dark/light\uff0c\u76ae\u80a4\uff1adefault/ares/mono/slate/poseidon/sisyphus/charizard\uff09',
|
||||
cmd_personality: '\u5207\u6362 Agent \u4eba\u8bbe',
|
||||
cmd_skills: '\u5217\u51fa\u53ef\u7528\u7684 Hermes \u6280\u80fd',
|
||||
available_commands: '\u53ef\u7528\u547d\u4ee4\uff1a',
|
||||
@@ -1167,6 +1170,7 @@ const LOCALES = {
|
||||
settings_label_model: '\u9ed8\u8ba4\u6a21\u578b',
|
||||
settings_label_send_key: '\u53d1\u9001\u5feb\u6377\u952e',
|
||||
settings_label_theme: '\u4e3b\u9898',
|
||||
settings_label_skin: '\u76ae\u80a4',
|
||||
settings_label_language: '\u8bed\u8a00',
|
||||
settings_label_token_usage: '\u663e\u793a token \u7528\u91cf',
|
||||
settings_label_bubble_layout: '聊天气泡布局',
|
||||
@@ -1499,7 +1503,7 @@ const LOCALES = {
|
||||
cmd_workspace: '\u6309\u540d\u7a31\u5207\u63db\u5de5\u4f5c\u5340',
|
||||
cmd_new: '\u65b0\u5efa\u804a\u5929\u6703\u8a71',
|
||||
cmd_usage: '\u5207\u63db token \u7528\u91cf\u986f\u793a',
|
||||
cmd_theme: '\u5207\u63db\u4e3b\u984c\uff08system/dark/light/slate/solarized/monokai/nord/oled\uff09',
|
||||
cmd_theme: '\u5207\u63db\u5916\u89c0\uff08\u4e3b\u984c\uff1asystem/dark/light\uff0c\u76ae\u819a\uff1adefault/ares/mono/slate/poseidon/sisyphus/charizard\uff09',
|
||||
cmd_personality: '\u5207\u63db Agent \u4eba\u8a2d',
|
||||
cmd_skills: '\u5217\u51fa\u53ef\u7528\u7684 Hermes \u6280\u80fd',
|
||||
available_commands: '\u53ef\u7528\u547d\u4ee4\uff1a',
|
||||
@@ -1562,6 +1566,7 @@ const LOCALES = {
|
||||
settings_label_model: '\u9ed8\u8a8d\u6a21\u578b',
|
||||
settings_label_send_key: '\u767c\u9001\u5feb\u6377\u9375',
|
||||
settings_label_theme: '\u4e3b\u984c',
|
||||
settings_label_skin: '\u76ae\u819a',
|
||||
settings_label_language: '\u8a9d\u8a00',
|
||||
settings_label_token_usage: '\u986f\u793a token \u7528\u91cf',
|
||||
settings_label_cli_sessions: '\u986f\u793a CLI \u6703\u8a71',
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<link rel="shortcut icon" href="static/favicon.ico">
|
||||
<!-- base href enables subpath mount support; all static paths must stay relative (no leading slash) -->
|
||||
<script>(function(){var p=location.pathname.endsWith('/')?location.pathname:(location.pathname.replace(/\/[^\/]*$/,'/')||'/');document.write('<base href="'+location.origin+p+'">');})()</script>
|
||||
<script>(function(){var t=localStorage.getItem('hermes-theme');if(t==='system'){t=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';}if(t&&t!=='dark')document.documentElement.dataset.theme=t;})()</script>
|
||||
<script>(function(){var themes={light:1,dark:1,system:1},skins={default:1,ares:1,mono:1,slate:1,poseidon:1,sisyphus:1,charizard:1},legacy={slate:['dark','slate'],solarized:['dark','poseidon'],monokai:['dark','sisyphus'],nord:['dark','slate'],oled:['dark','default']},t=(localStorage.getItem('hermes-theme')||'dark').toLowerCase(),s=(localStorage.getItem('hermes-skin')||'').toLowerCase(),m=legacy[t],theme=m?m[0]:(themes[t]?t:'dark'),skin=skins[s]?s:(m?m[1]:'default');localStorage.setItem('hermes-theme',theme);localStorage.setItem('hermes-skin',skin);if(theme==='system')theme=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';if(theme==='dark')document.documentElement.classList.add('dark');if(skin!=='default')document.documentElement.dataset.skin=skin;})()</script>
|
||||
<script>(function(){try{document.documentElement.dataset.workspacePanel=localStorage.getItem('hermes-webui-workspace-panel')==='open'?'open':'closed';}catch(e){document.documentElement.dataset.workspacePanel='closed';}})()</script>
|
||||
<link rel="stylesheet" href="static/style.css">
|
||||
<!-- KaTeX math rendering CSS (loaded eagerly to prevent layout shift) -->
|
||||
@@ -305,14 +305,14 @@
|
||||
<div class="composer-ws-wrap">
|
||||
<button class="composer-workspace-chip ws-chip" id="composerWorkspaceChip" type="button" onclick="toggleComposerWsDropdown()" title="Switch workspace" disabled>
|
||||
<span class="composer-workspace-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span>
|
||||
<span class="composer-workspace-label" id="composerWorkspaceLabel">Workspace</span>
|
||||
<span class="composer-workspace-label" id="composerWorkspaceLabel"></span>
|
||||
<span class="composer-workspace-chevron" aria-hidden="true"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="composer-model-wrap">
|
||||
<button class="composer-model-chip" id="composerModelChip" type="button" onclick="toggleModelDropdown()" title="Conversation model">
|
||||
<span class="composer-model-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M15 2v2"/><path d="M15 20v2"/><path d="M2 15h2"/><path d="M2 9h2"/><path d="M20 15h2"/><path d="M20 9h2"/><path d="M9 2v2"/><path d="M9 20v2"/></svg></span>
|
||||
<span class="composer-model-label" id="composerModelLabel">Model</span>
|
||||
<span class="composer-model-label" id="composerModelLabel"></span>
|
||||
<span class="composer-model-chevron" aria-hidden="true"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||
</button>
|
||||
<select id="modelSelect" class="composer-model-select" title="Conversation model" aria-hidden="true" tabindex="-1">
|
||||
@@ -439,6 +439,10 @@
|
||||
<svg class="settings-tab-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
<span class="settings-tab-title">Conversation</span>
|
||||
</button>
|
||||
<button class="settings-tab" id="settingsTabAppearance" type="button" role="tab" aria-selected="false" aria-controls="settingsPaneAppearance" onclick="switchSettingsSection('appearance')">
|
||||
<svg class="settings-tab-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
||||
<span class="settings-tab-title">Appearance</span>
|
||||
</button>
|
||||
<button class="settings-tab" id="settingsTabPreferences" type="button" role="tab" aria-selected="false" aria-controls="settingsPanePreferences" onclick="switchSettingsSection('preferences')">
|
||||
<svg class="settings-tab-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg>
|
||||
<span class="settings-tab-title">Preferences</span>
|
||||
@@ -464,6 +468,45 @@
|
||||
</div>
|
||||
<input type="file" id="importFileInput" accept=".json" style="display:none">
|
||||
</div>
|
||||
<div class="settings-pane" id="settingsPaneAppearance" role="tabpanel" aria-labelledby="settingsTabAppearance">
|
||||
<div class="settings-section-head">
|
||||
<div>
|
||||
<div class="settings-section-title">Appearance</div>
|
||||
<div class="settings-section-meta">Theme, accent colors, and visual style.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label data-i18n="settings_label_theme">Theme</label>
|
||||
<div id="themePickerGrid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:4px">
|
||||
<button type="button" data-theme-val="light" onclick="_pickTheme('light')" class="theme-pick-btn" style="border:1px solid var(--border2);border-radius:10px;padding:10px 8px;text-align:center;cursor:pointer;background:none;transition:all .15s">
|
||||
<div style="width:100%;height:40px;border-radius:6px;background:#fff;border:1px solid rgba(0,0,0,.12);margin-bottom:6px;display:flex;align-items:center;justify-content:center">
|
||||
<svg width="16" height="16" fill="none" stroke="#999" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
||||
</div>
|
||||
<span style="font-size:12px;font-weight:500;color:var(--text)">Light</span>
|
||||
</button>
|
||||
<button type="button" data-theme-val="dark" onclick="_pickTheme('dark')" class="theme-pick-btn" style="border:1px solid var(--border2);border-radius:10px;padding:10px 8px;text-align:center;cursor:pointer;background:none;transition:all .15s">
|
||||
<div style="width:100%;height:40px;border-radius:6px;background:#1a1a2e;border:1px solid rgba(255,255,255,.1);margin-bottom:6px;display:flex;align-items:center;justify-content:center">
|
||||
<svg width="16" height="16" fill="none" stroke="#666" stroke-width="2" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1111.21 3a7 7 0 009.79 9.79z"/></svg>
|
||||
</div>
|
||||
<span style="font-size:12px;font-weight:500;color:var(--text)">Dark</span>
|
||||
</button>
|
||||
<button type="button" data-theme-val="system" onclick="_pickTheme('system')" class="theme-pick-btn" style="border:1px solid var(--border2);border-radius:10px;padding:10px 8px;text-align:center;cursor:pointer;background:none;transition:all .15s">
|
||||
<div style="width:100%;height:40px;border-radius:6px;background:linear-gradient(to right,#fff,#1a1a2e);border:1px solid rgba(0,0,0,.12);margin-bottom:6px;display:flex;align-items:center;justify-content:center">
|
||||
<svg width="16" height="16" fill="none" stroke="#888" stroke-width="2" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
|
||||
</div>
|
||||
<span style="font-size:12px;font-weight:500;color:var(--text)">System</span>
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" id="settingsTheme" value="dark">
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label data-i18n="settings_label_skin">Skin</label>
|
||||
<div id="skinPickerGrid" style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-top:4px">
|
||||
</div>
|
||||
<input type="hidden" id="settingsSkin" value="default">
|
||||
</div>
|
||||
<button class="sm-btn" onclick="saveSettings()" style="margin-top:12px;width:100%;padding:8px;font-weight:600" data-i18n="settings_save_btn">Save Settings</button>
|
||||
</div>
|
||||
<div class="settings-pane" id="settingsPanePreferences" role="tabpanel" aria-labelledby="settingsTabPreferences">
|
||||
<div class="settings-section-head">
|
||||
<div>
|
||||
@@ -482,19 +525,6 @@
|
||||
<option value="ctrl+enter">Ctrl+Enter (Enter for newline)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="settingsTheme" data-i18n="settings_label_theme">Theme</label>
|
||||
<select id="settingsTheme" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px" onchange="_applyTheme(this.value)">
|
||||
<option value="system">System (auto)</option>
|
||||
<option value="dark">Dark (default)</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="slate">Slate (charcoal)</option>
|
||||
<option value="solarized">Solarized Dark</option>
|
||||
<option value="monokai">Monokai</option>
|
||||
<option value="nord">Nord</option>
|
||||
<option value="oled">OLED</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="settingsLanguage" data-i18n="settings_label_language">Language</label>
|
||||
<select id="settingsLanguage" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px"></select>
|
||||
@@ -561,7 +591,7 @@
|
||||
<div class="settings-section-title">System</div>
|
||||
<div class="settings-section-meta">Instance version and access controls.</div>
|
||||
</div>
|
||||
<span class="settings-version-badge">v0.50.76</span>
|
||||
<span class="settings-version-badge">v0.50.77</span>
|
||||
</div>
|
||||
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
|
||||
<label for="settingsPassword" data-i18n="settings_label_password">Access Password</label>
|
||||
|
||||
@@ -50,7 +50,7 @@ async function loadCrons() {
|
||||
? `<button class="cron-btn" onclick="cronResume('${job.id}')">${li('play',12)} ${esc(t('cron_resume'))}</button>`
|
||||
: `<button class="cron-btn pause" onclick="cronPause('${job.id}')">${li('pause',12)} ${esc(t('cron_pause'))}</button>`}
|
||||
<button class="cron-btn" onclick="cronEditOpen('${job.id}',${JSON.stringify(job).replace(/"/g,'"')})">${li('pencil',12)} ${esc(t('edit'))}</button>
|
||||
<button class="cron-btn" style="border-color:rgba(201,168,76,.3);color:var(--accent)" onclick="cronDelete('${job.id}')">${li('trash-2',12)} ${esc(t('delete_title'))}</button>
|
||||
<button class="cron-btn" style="border-color:var(--accent-bg-strong);color:var(--accent-text)" onclick="cronDelete('${job.id}')">${li('trash-2',12)} ${esc(t('delete_title'))}</button>
|
||||
</div>
|
||||
<!-- Inline edit form, hidden by default -->
|
||||
<div id="cron-edit-${job.id}" style="display:none;margin-top:8px;border-top:1px solid var(--border);padding-top:8px">
|
||||
@@ -547,7 +547,9 @@ function syncWorkspaceDisplays(){
|
||||
const composerLabel=$('composerWorkspaceLabel');
|
||||
const composerDropdown=$('composerWsDropdown');
|
||||
if(!hasSession && composerDropdown) composerDropdown.classList.remove('open');
|
||||
if(composerLabel) composerLabel.textContent=label;
|
||||
// Only show workspace label once boot has finished to prevent
|
||||
// flash of "No workspace" before the saved session finishes loading.
|
||||
if(composerLabel) composerLabel.textContent=S._bootReady?label:'';
|
||||
if(composerChip){
|
||||
composerChip.disabled=!hasSession;
|
||||
composerChip.title=hasSession?ws:t('no_workspace');
|
||||
@@ -1068,13 +1070,14 @@ document.addEventListener('drop',e=>{e.preventDefault();dragCounter=0;wrap.class
|
||||
|
||||
let _settingsDirty = false;
|
||||
let _settingsThemeOnOpen = null; // track theme at open time for discard revert
|
||||
let _settingsSkinOnOpen = null; // track skin at open time for discard revert
|
||||
let _settingsSection = 'conversation';
|
||||
|
||||
function switchSettingsSection(name){
|
||||
const section=(name==='preferences'||name==='system')?name:'conversation';
|
||||
const section=(name==='appearance'||name==='preferences'||name==='system')?name:'conversation';
|
||||
_settingsSection=section;
|
||||
const map={conversation:'Conversation',preferences:'Preferences',system:'System'};
|
||||
['conversation','preferences','system'].forEach(key=>{
|
||||
const map={conversation:'Conversation',appearance:'Appearance',preferences:'Preferences',system:'System'};
|
||||
['conversation','appearance','preferences','system'].forEach(key=>{
|
||||
const tab=$('settingsTab'+map[key]);
|
||||
const pane=$('settingsPane'+map[key]);
|
||||
const active=key===section;
|
||||
@@ -1112,7 +1115,8 @@ function toggleSettings(){
|
||||
if(!overlay) return;
|
||||
if(overlay.style.display==='none'){
|
||||
_settingsDirty = false;
|
||||
_settingsThemeOnOpen = localStorage.getItem('hermes-theme') || document.documentElement.dataset.theme || 'dark';
|
||||
_settingsThemeOnOpen = localStorage.getItem('hermes-theme') || 'dark';
|
||||
_settingsSkinOnOpen = localStorage.getItem('hermes-skin') || 'default';
|
||||
_settingsSection = 'conversation';
|
||||
overlay.style.display='';
|
||||
loadSettingsPanel();
|
||||
@@ -1152,7 +1156,10 @@ function _revertSettingsPreview(){
|
||||
if(_settingsThemeOnOpen){
|
||||
localStorage.setItem('hermes-theme', _settingsThemeOnOpen);
|
||||
if(typeof _applyTheme==='function') _applyTheme(_settingsThemeOnOpen);
|
||||
else document.documentElement.dataset.theme = _settingsThemeOnOpen;
|
||||
}
|
||||
if(_settingsSkinOnOpen){
|
||||
localStorage.setItem('hermes-skin', _settingsSkinOnOpen);
|
||||
if(typeof _applySkin==='function') _applySkin(_settingsSkinOnOpen);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1187,6 +1194,16 @@ function _markSettingsDirty(){
|
||||
async function loadSettingsPanel(){
|
||||
try{
|
||||
const settings=await api('/api/settings');
|
||||
// Hydrate appearance controls first so a slow /api/models request
|
||||
// cannot overwrite an in-progress theme/skin selection.
|
||||
const themeSel=$('settingsTheme');
|
||||
const themeVal=settings.theme||'dark';
|
||||
if(themeSel) themeSel.value=themeVal;
|
||||
if(typeof _syncThemePicker==='function') _syncThemePicker(themeVal);
|
||||
const skinVal=(settings.skin||'default').toLowerCase();
|
||||
const skinSel=$('settingsSkin');
|
||||
if(skinSel) skinSel.value=skinVal;
|
||||
if(typeof _buildSkinPicker==='function') _buildSkinPicker(skinVal);
|
||||
const resolvedLanguage=(typeof resolvePreferredLocale==='function')
|
||||
? resolvePreferredLocale(settings.language, localStorage.getItem('hermes-lang'))
|
||||
: (settings.language || localStorage.getItem('hermes-lang') || 'en');
|
||||
@@ -1218,9 +1235,6 @@ async function loadSettingsPanel(){
|
||||
// Send key preference
|
||||
const sendKeySel=$('settingsSendKey');
|
||||
if(sendKeySel){sendKeySel.value=settings.send_key||'enter';sendKeySel.addEventListener('change',_markSettingsDirty,{once:false});}
|
||||
// Theme preference
|
||||
const themeSel=$('settingsTheme');
|
||||
if(themeSel){themeSel.value=settings.theme||'dark';themeSel.addEventListener('change',_markSettingsDirty,{once:false});}
|
||||
// Language preference — populate from LOCALES bundle
|
||||
const langSel=$('settingsLanguage');
|
||||
if(langSel){
|
||||
@@ -1275,7 +1289,7 @@ function _setSettingsAuthButtonsVisible(active){
|
||||
}
|
||||
|
||||
function _applySavedSettingsUi(saved, body, opts){
|
||||
const {sendKey,showTokenUsage,showCliSessions,theme,language}=opts;
|
||||
const {sendKey,showTokenUsage,showCliSessions,theme,skin,language}=opts;
|
||||
window._sendKey=sendKey||'enter';
|
||||
window._showTokenUsage=showTokenUsage;
|
||||
window._showCliSessions=showCliSessions;
|
||||
@@ -1293,6 +1307,7 @@ function _applySavedSettingsUi(saved, body, opts){
|
||||
_setSettingsAuthButtonsVisible(!!saved.auth_enabled);
|
||||
_settingsDirty=false;
|
||||
_settingsThemeOnOpen=theme;
|
||||
_settingsSkinOnOpen=skin||'default';
|
||||
const bar=$('settingsUnsavedBar');
|
||||
if(bar) bar.style.display='none';
|
||||
renderMessages();
|
||||
@@ -1307,12 +1322,14 @@ async function saveSettings(andClose){
|
||||
const showCliSessions=!!($('settingsShowCliSessions')||{}).checked;
|
||||
const pw=($('settingsPassword')||{}).value;
|
||||
const theme=($('settingsTheme')||{}).value||'dark';
|
||||
const skin=($('settingsSkin')||{}).value||'default';
|
||||
const language=($('settingsLanguage')||{}).value||'en';
|
||||
const body={};
|
||||
if(model) body.default_model=model;
|
||||
|
||||
if(sendKey) body.send_key=sendKey;
|
||||
body.theme=theme;
|
||||
body.skin=skin;
|
||||
body.language=language;
|
||||
body.show_token_usage=showTokenUsage;
|
||||
body.show_cli_sessions=showCliSessions;
|
||||
@@ -1328,7 +1345,7 @@ async function saveSettings(andClose){
|
||||
if(pw && pw.trim()){
|
||||
try{
|
||||
const saved=await api('/api/settings',{method:'POST',body:JSON.stringify({...body,_set_password:pw.trim()})});
|
||||
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,language});
|
||||
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,skin,language});
|
||||
showToast(t(saved.auth_just_enabled?'settings_saved_pw':'settings_saved_pw_updated'));
|
||||
_hideSettingsPanel();
|
||||
return;
|
||||
@@ -1336,7 +1353,7 @@ async function saveSettings(andClose){
|
||||
}
|
||||
try{
|
||||
const saved=await api('/api/settings',{method:'POST',body:JSON.stringify(body)});
|
||||
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,language});
|
||||
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,skin,language});
|
||||
showToast(t('settings_saved'));
|
||||
_hideSettingsPanel();
|
||||
}catch(e){
|
||||
|
||||
643
static/style.css
643
static/style.css
File diff suppressed because it is too large
Load Diff
@@ -191,6 +191,8 @@ function syncModelChip(){
|
||||
const label=$('composerModelLabel');
|
||||
const dd=$('composerModelDropdown');
|
||||
if(!sel||!chip||!label) return;
|
||||
// Don't show a model label until boot has finished loading to prevent flash of wrong default
|
||||
if(!S._bootReady){ label.textContent=''; chip.title='Conversation model'; return; }
|
||||
const opt=_selectedModelOption();
|
||||
label.textContent=opt?opt.textContent:getModelLabel(sel.value||'');
|
||||
chip.title=sel.value||'Conversation model';
|
||||
@@ -654,6 +656,8 @@ function updateSendBtn(){
|
||||
if(canSend&&!btn.classList.contains('visible')){
|
||||
btn.classList.remove('visible');
|
||||
requestAnimationFrame(()=>btn.classList.add('visible'));
|
||||
} else if(!canSend){
|
||||
btn.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
function setBusy(v){
|
||||
@@ -1635,7 +1639,7 @@ function renderMermaidBlocks(){
|
||||
script.crossOrigin='anonymous';
|
||||
script.onload=()=>{
|
||||
if(typeof mermaid!=='undefined'){
|
||||
mermaid.initialize({startOnLoad:false,theme:'dark',themeVariables:{
|
||||
mermaid.initialize({startOnLoad:false,theme:document.documentElement.classList.contains('dark')?'dark':'default',themeVariables:{
|
||||
primaryColor:'#4a6fa5',primaryTextColor:'#e2e8f0',lineColor:'#718096',
|
||||
secondaryColor:'#2d3748',tertiaryColor:'#1a202c',primaryBorderColor:'#4a5568',
|
||||
}});
|
||||
|
||||
Reference in New Issue
Block a user