feat: add Sienna skin (warm clay & sand earth palette)

Adds a new opt-in skin with a warm clay accent (#D97757) on a soft sand
background — earth-toned palette inspired by classic terracotta paints,
suitable for users who prefer a calmer reading surface than the default
gold accent.

What ships:
  * static/style.css: full :root[data-skin="sienna"] palette block (light +
    dark variants), with --user-bubble-* tokens for the redesigned user
    bubble, neutral tool-card chrome, and a specificity-boosted .new-chat-btn
    override that prevents clay-on-clay invisible-text in light mode.
  * static/boot.js: Sienna entry in the _SKINS picker list.
  * static/index.html: 'sienna' added to the early-init skin allowlist so
    saved skin survives boot.

What is intentionally NOT included:
  * No theme default change. Default theme stays 'dark'.
  * No forced migration. Users on the default skin stay on default; opt in
    via Settings then Skin to switch.

Tests: tests/test_sienna_skin.py — 6 assertions covering palette presence,
allowlist registration, no-forced-migration guard, default-theme stability,
and the .new-chat-btn specificity guard.

Full suite: 3258 passed.

Salvaged from contributor work in PR #1084 — the warm-palette skin idea
was good. Renamed to avoid trademark confusion, scoped down to a clean
opt-in skin with no forced rollout, and stripped of unrelated changes.

Co-authored-by: Hermes Agent <hermes@get-hermes.ai>
This commit is contained in:
Hermes Agent
2026-04-30 03:43:13 +00:00
parent 20ac6dfe5c
commit 5cd79c8025
4 changed files with 142 additions and 1 deletions
+1
View File
@@ -639,6 +639,7 @@ const _SKINS=[
{name:'Poseidon', colors:['#0EA5E9','#0284C7','#0369A1']}, {name:'Poseidon', colors:['#0EA5E9','#0284C7','#0369A1']},
{name:'Sisyphus', colors:['#A78BFA','#8B5CF6','#7C3AED']}, {name:'Sisyphus', colors:['#A78BFA','#8B5CF6','#7C3AED']},
{name:'Charizard',colors:['#FB923C','#F97316','#EA580C']}, {name:'Charizard',colors:['#FB923C','#F97316','#EA580C']},
{name:'Sienna', colors:['#D97757','#C06A49','#9A523A']},
]; ];
const _VALID_THEMES=new Set((_THEMES||[]).map(t=>t.value)); const _VALID_THEMES=new Set((_THEMES||[]).map(t=>t.value));
const _VALID_SKINS=new Set((_SKINS||[]).map(s=>s.name.toLowerCase())); const _VALID_SKINS=new Set((_SKINS||[]).map(s=>s.name.toLowerCase()));
+1 -1
View File
@@ -15,7 +15,7 @@
<link rel="apple-touch-icon" href="static/favicon.svg"> <link rel="apple-touch-icon" href="static/favicon.svg">
<!-- base href enables subpath mount support; all static paths must stay relative (no leading slash) --> <!-- 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 p=location.pathname.endsWith('/')?location.pathname:(location.pathname.replace(/\/[^\/]*$/,'/')||'/');document.write('<base href="'+location.origin+p+'">');})()</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(){var themes={light:1,dark:1,system:1},skins={default:1,ares:1,mono:1,slate:1,poseidon:1,sisyphus:1,charizard:1,sienna: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(){var fs=localStorage.getItem('hermes-font-size');if(fs&&fs!=='default')document.documentElement.dataset.fontSize=fs;})()</script> <script>(function(){var fs=localStorage.getItem('hermes-font-size');if(fs&&fs!=='default')document.documentElement.dataset.fontSize=fs;})()</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> <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"> <link rel="stylesheet" href="static/style.css">
+61
View File
@@ -98,6 +98,67 @@
/* ── Skin: Charizard (orange) ── */ /* ── Skin: Charizard (orange) ── */
:root[data-skin="charizard"]{--accent:#EA580C;--accent-hover:#C2410C;--accent-bg:rgba(234,88,12,0.08);--accent-bg-strong:rgba(234,88,12,0.15);--accent-text:#C2410C;} :root[data-skin="charizard"]{--accent:#EA580C;--accent-hover:#C2410C;--accent-bg:rgba(234,88,12,0.08);--accent-bg-strong:rgba(234,88,12,0.15);--accent-text:#C2410C;}
:root.dark[data-skin="charizard"]{--accent:#FB923C;--accent-hover:#F97316;--accent-bg:rgba(251,146,60,0.08);--accent-bg-strong:rgba(251,146,60,0.15);--accent-text:#FB923C;} :root.dark[data-skin="charizard"]{--accent:#FB923C;--accent-hover:#F97316;--accent-bg:rgba(251,146,60,0.08);--accent-bg-strong:rgba(251,146,60,0.15);--accent-text:#FB923C;}
/* Skin: Sienna (warm clay & sand earth palette)
Full palette rewrite (not just --accent): warm off-white surface,
soft sand sidebar, clay accent. Sets --user-bubble-* for the redesigned
user bubble block, mutes tool-card chrome to neutral, and bumps the
reading column to a serif body for long-form prose. Opt-in via the
Settings Skin picker; default skin stays "default" (gold). */
:root[data-skin="sienna"]{
--bg:#FAF9F5;--sidebar:#F0EEE6;--surface:#FFFFFF;
--border:#E7E4DB;--border2:#D7D3C7;
--text:#1F1E1C;--muted:#6B6A63;--strong:#141311;--em:#3E3D39;
--accent:#D97757;--accent-hover:#BF6545;--accent-text:#A55237;
--accent-bg:rgba(217,119,87,0.09);--accent-bg-strong:rgba(217,119,87,0.18);
--code-bg:#F5F3EC;--code-inline-bg:rgba(20,19,17,0.06);--code-text:#8A3E1A;--pre-text:#1F1E1C;
--topbar-bg:rgba(250,249,245,0.96);--main-bg:rgba(250,249,245,0.55);
--input-bg:rgba(20,19,17,0.035);--hover-bg:rgba(20,19,17,0.05);
--focus-ring:rgba(217,119,87,0.3);--focus-glow:rgba(217,119,87,0.1);
--blue:#2E6F9E;--gold:#A55237;
--user-bubble-bg:#ECE9DF;--user-bubble-border:#DED9CC;
--user-bubble-text:#1F1E1C;--user-bubble-placeholder:#6B6A63;
--user-selection-bg:rgba(217,119,87,0.28);--user-selection-text:#1F1E1C;
}
:root.dark[data-skin="sienna"]{
--bg:#1F1E1C;--sidebar:#262522;--surface:#2C2B28;
--border:#3A3935;--border2:#4A4843;
--text:#EDEBE3;--muted:#A3A197;--strong:#F7F5ED;--em:#C9C6BC;
--accent:#E0896D;--accent-hover:#D97757;--accent-text:#E6A88A;
--accent-bg:rgba(224,137,109,0.12);--accent-bg-strong:rgba(224,137,109,0.22);
--code-bg:#2A2926;--code-inline-bg:rgba(255,255,255,0.07);--code-text:#F0B593;--pre-text:#EDEBE3;
--topbar-bg:rgba(38,37,34,0.96);--main-bg:rgba(31,30,28,0.55);
--input-bg:rgba(255,255,255,0.045);--hover-bg:rgba(255,255,255,0.07);
--focus-ring:rgba(224,137,109,0.35);--focus-glow:rgba(224,137,109,0.1);
--blue:#8BB8D6;--gold:#E6A88A;
--user-bubble-bg:#34322E;--user-bubble-border:#42403B;
--user-bubble-text:#EDEBE3;--user-bubble-placeholder:#A3A197;
--user-selection-bg:rgba(224,137,109,0.3);--user-selection-text:#F7F5ED;
}
/* Specificity boosted to beat the :root:not(.dark) .new-chat-btn rule that
sets color:var(--accent-text) without this, the solid-accent background
would collide with the inherited text color, producing clay-on-clay text. */
:root[data-skin="sienna"]:not(.dark) .new-chat-btn,
:root.dark[data-skin="sienna"] .new-chat-btn{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600;box-shadow:0 1px 2px rgba(20,19,17,0.08);}
:root[data-skin="sienna"]:not(.dark) .new-chat-btn:hover,
:root.dark[data-skin="sienna"] .new-chat-btn:hover{background:var(--accent-hover);border-color:var(--accent-hover);color:#fff;}
/* Tool / thinking cards: mute the blue-ish Hermes highlights to neutral */
:root[data-skin="sienna"] .tool-card{background:rgba(20,19,17,0.025);border-color:var(--border);border-radius:10px;}
:root[data-skin="sienna"] .tool-card:hover{border-color:var(--border2);}
:root[data-skin="sienna"] .tool-card-running{background:var(--accent-bg);border-color:var(--accent-bg-strong);}
:root[data-skin="sienna"] .tool-card-name{color:var(--muted);font-weight:500;}
:root[data-skin="sienna"] .tool-arg-key{color:var(--accent-text);}
:root[data-skin="sienna"] .tool-card-more{color:var(--accent-text);}
:root[data-skin="sienna"] .tool-card-running-dot{background:var(--accent);}
:root.dark[data-skin="sienna"] .tool-card{background:rgba(255,255,255,0.03);}
/* User bubble: soft sand pill, not a saturated accent fill */
:root[data-skin="sienna"] .msg-row[data-role="user"] .msg-body{border-radius:16px;padding:12px 16px;}
:root[data-skin="sienna"] .msg-row[data-role="user"] .msg-body code{color:var(--user-bubble-text);background:rgba(20,19,17,0.08);}
/* Send button pop and active session indicator */
:root[data-skin="sienna"] button.send-btn {box-shadow:0 1px 3px rgba(217,119,87,0.3);}
:root[data-skin="sienna"] button.send-btn:hover {box-shadow:0 2px 8px rgba(217,119,87,0.45);}
:root[data-skin="sienna"] .session-item.active{border-left:2px solid var(--accent);}
:root[data-skin="sienna"] .composer-box{border-radius:18px;}
/* #594: app-dialog light mode overrides — base styles use hardcoded dark gradients */ /* #594: app-dialog light mode overrides — base styles use hardcoded dark gradients */
:root:not(.dark) .app-dialog{ :root:not(.dark) .app-dialog{
background:linear-gradient(180deg,rgba(240,237,232,.99),rgba(228,224,216,.99)); background:linear-gradient(180deg,rgba(240,237,232,.99),rgba(228,224,216,.99));
+79
View File
@@ -0,0 +1,79 @@
"""Sienna skin: warm clay/sand earth palette, opt-in via Settings → Skin."""
from pathlib import Path
REPO = Path(__file__).parent.parent
CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
INDEX_HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8")
def test_sienna_skin_present_in_skins_list():
"""The Sienna skin must be exposed in the picker grid via _SKINS."""
assert "{name:'Sienna'" in BOOT_JS, "Sienna skin missing from _SKINS list"
assert "'#D97757','#C06A49','#9A523A'" in BOOT_JS, (
"Sienna preview swatches missing"
)
def test_sienna_skin_in_early_init_allowlist():
"""The early-init skin allowlist must accept 'sienna'."""
assert "sienna:1" in INDEX_HTML, (
"Sienna missing from early-init skin allowlist; saved skin would be "
"rejected and reset to default on boot"
)
def test_sienna_skin_palette_has_full_light_and_dark():
"""Sienna defines both light and dark scoped palettes."""
assert ':root[data-skin="sienna"]{' in CSS, (
"Sienna light-mode palette block missing"
)
assert ':root.dark[data-skin="sienna"]{' in CSS, (
"Sienna dark-mode palette block missing"
)
# Spot-check that the palette is a full rewrite (not just --accent)
for token in ("--bg:#FAF9F5", "--sidebar:#F0EEE6", "--accent:#D97757"):
assert token in CSS, f"Sienna light palette token missing: {token}"
for token in ("--bg:#1F1E1C", "--sidebar:#262522", "--accent:#E0896D"):
assert token in CSS, f"Sienna dark palette token missing: {token}"
def test_sienna_skin_does_not_force_migration():
"""Sienna must not be silently migrated onto existing users.
The early-init script in index.html must NOT contain logic that flips an
existing 'default' skin to 'sienna' on first load. New users keep the gold
default; users opt in via Settings Skin.
"""
# The skin allowlist line should NOT contain a sienna-migration flag.
init_script_idx = INDEX_HTML.find("var themes=")
end_idx = INDEX_HTML.find("</script>", init_script_idx)
init_block = INDEX_HTML[init_script_idx:end_idx]
forbidden = ["sienna-migrated", "skin-sienna-migrated", "skin='sienna'", 'skin="sienna"']
for marker in forbidden:
assert marker not in init_block, (
f"Sienna skin must be opt-in, not force-migrated. Found '{marker}' "
f"in early-init script."
)
def test_default_theme_is_still_dark():
"""Adding a new skin must not change the default theme."""
# The early-init script defaults to 'dark' when no saved theme exists.
init_script_idx = INDEX_HTML.find("var themes=")
end_idx = INDEX_HTML.find("</script>", init_script_idx)
init_block = INDEX_HTML[init_script_idx:end_idx]
assert "||'dark'" in init_block, (
"Default theme must remain 'dark' (the existing baseline)"
)
def test_sienna_new_chat_button_specificity_guards_against_clay_on_clay():
"""The new-chat button needs higher specificity than the base
:root:not(.dark) .new-chat-btn rule, otherwise the inherited
color:var(--accent-text) collides with the solid-accent background and
produces invisible clay-on-clay text in light mode."""
assert ':root[data-skin="sienna"]:not(.dark) .new-chat-btn' in CSS, (
"Sienna light-mode .new-chat-btn override missing — clay-on-clay risk"
)