feat: add Geist Contrast skin

This commit is contained in:
Eleanor Berger
2026-05-18 13:10:17 +02:00
committed by nesquena-hermes
parent c8896ac1b6
commit 4598adfd04
11 changed files with 247 additions and 22 deletions
+7
View File
@@ -3,6 +3,7 @@
## [Unreleased]
<<<<<<< HEAD
## [v0.51.94] — 2026-05-19 — Release BR (stage-387 — 10-PR full sweep batch — Slice 4b runner adapter facade + folder zip download + partial recovery marker dedupe + browser api() client-side timeout + auto-compression card rotation finish + composer draft rollback fix + metadata count reconciliation + active-session refresh on external sidecar updates + indexed context metadata + gateway-queues approval peek)
### Fixed
@@ -42,6 +43,8 @@
### Documentation
- **PR #2575** by @Michaelyklam (refs #1925) — Advance the runtime-adapter RFC to the Slice 4 runner/sidecar planning gate after #2560 shipped the queue-staging clarification. The RFC now marks queue routing as staged by default, defines Slice 4a as a docs/test contract before any runner code lands, and pins default-off feature-flagging, restart/reattach success criteria, control parity, profile/workspace payload isolation, and explicit non-goals for legacy-backend removal or server-side queue scheduler work.
=======
>>>>>>> 2ecdd66d (feat: add Geist Contrast skin)
## [v0.51.92] — 2026-05-19 — Release BP (stage-385 — 7-PR full sweep batch — RFC Slice 3c clarification + workspace tree icon alignment + project move cache refresh + auto-compression handoff metadata + Grok OAuth provider catalog + anonymous custom endpoint picker fallback + PWA standalone reload + pull-to-refresh)
@@ -94,6 +97,10 @@
- **PR #2511** by @franksong2702 (refs #2502 / #2503) — Update the `docs/ui-ux/` demo appearance controls to initialize as `class="dark" data-skin="slate"` instead of the deprecated `data-theme`-only buttons and legacy theme names. Brings the demo pages in line with the live Theme + Skin contract referenced from the new `docs/CONTRACTS.md` so contributors following the contract-index path don't land on stale demos.
- **PR #2509** by @Michaelyklam (refs #1925) — Advance the runtime-adapter RFC after the Slice 3b approval/clarify seam shipped in v0.51.89. The RFC now marks Slice 3b as shipped and defines the next Slice 3c queue/continue + goal control gate: route those controls through `RuntimeAdapter.queue_message(...)` / `update_goal(...)` only after pinning stable response contracts, bounded unavailable-control behavior, replayable lifecycle/status evidence, ordering/idempotency expectations, and explicit non-goals for runner/sidecar ownership or a WebUI-owned queue/goal scheduler. Docs + adapter-seam regression test only — no runtime/control routing changes in this PR.
### Added
- **Geist Contrast skin** — Add a new Geist-inspired `geist-contrast` skin with neutral monochrome surfaces, restrained selected/sidebar states, and dark-mode `#FFF175` primary accents with black foreground on solid accent controls.
## [v0.51.89] — 2026-05-18 — Release BM (stage-382 — 6-PR full sweep batch — runtime adapter approval/clarify seam + SOUL.md memory panel + #1855 resolve_model_provider fast-path + PWA sidebar spinner fix + /model active-provider preference + contributor contract docs index)
### Changed
+1 -1
View File
@@ -469,7 +469,7 @@ Production data and real cron jobs are never touched. Current snapshot:
### Themes
- Appearance is split into two axes: Theme (`system`, `dark`, `light`) and Skin
(`default`, `ares`, `mono`, `slate`, `poseidon`, `sisyphus`, `charizard`,
`sienna`, `catppuccin`, `nous`)
`sienna`, `catppuccin`, `nous`, `geist-contrast` / Geist Contrast)
- Switch via Settings -> Appearance (instant live preview) or `/theme <theme-or-skin>`
- Persists across reloads (server-side in settings.json + localStorage for flicker-free loading)
- Skins use `data-skin` plus CSS variables; dark mode resolves through the
+4 -3
View File
@@ -4,7 +4,7 @@ Hermes Web UI splits **appearance** into two independent pickers:
- **Theme** — the mode: `System`, `Dark`, or `Light`. Drives the background,
text, surface, and chrome colors.
- **Skin** — the accent palette: ten named skins ship built-in. Drives only
- **Skin** — the accent palette: eleven named skins ship built-in. Drives only
the `--accent` family (active states, links, focus rings, primary actions).
You pick one of each and they combine, so the look adapts to your environment
@@ -15,13 +15,13 @@ without losing your favorite accent — pure CSS, no Python changes needed.
## Switching Appearance
**Settings panel:** Click the gear icon → **Appearance**. The **Theme** card
toggles Light/Dark/System; the **Skin** grid offers ten accent palettes.
toggles Light/Dark/System; the **Skin** grid offers eleven accent palettes.
Preview is instant — the UI updates as you click.
**Slash command:** Type `/theme <name>` in the composer. The command accepts
both theme names (`system`, `dark`, `light`) and skin names (`default`, `ares`,
`mono`, `slate`, `poseidon`, `sisyphus`, `charizard`, `sienna`,
`catppuccin`, `nous`). It updates the matching axis and leaves the other one
`catppuccin`, `nous`, `geist-contrast`). It updates the matching axis and leaves the other one
alone.
**Persistence:** Both choices are stored in `localStorage` for flicker-free
@@ -57,6 +57,7 @@ absent for light. System mode tracks the OS preference at runtime.
| **Sienna** | Warm clay and sand earth palette. Soft and natural. |
| **Catppuccin** | Catppuccin Latte/Mocha palette with Mauve accent. |
| **Nous** | Steel-blue accent with dashed technical surfaces. |
| **Geist Contrast** (`geist-contrast`) | Geist-inspired monochrome surfaces with a restrained dark-mode `#FFF175` accent. |
Each skin defines paired light + dark variants so it reads cleanly on either
theme. The skin is applied as `data-skin="<name>"` on `<html>` (the default
+1
View File
@@ -4279,6 +4279,7 @@ _SETTINGS_SKIN_VALUES = {
"sienna",
"catppuccin",
"nous",
"geist-contrast",
}
_SETTINGS_LEGACY_THEME_MAP = {
# Legacy full themes now map onto the closest supported theme + accent skin pair.
+1 -1
View File
@@ -150,7 +150,7 @@ Current implementation has two appearance axes, sourced from `static/boot.js`:
`theme` is only `light`, `dark`, or `system` and resolves to the `.dark` class
for dark mode; `skin` is a separate axis applied with `data-skin` and currently
includes `default`, `ares`, `mono`, `slate`, `poseidon`, `sisyphus`,
`charizard`, `sienna`, `catppuccin`, and `nous`. `slate` is both an active skin
`charizard`, `sienna`, `catppuccin`, `nous`, and `geist-contrast` / Geist Contrast. `slate` is both an active skin
and a legacy theme-name migration target; `solarized`, `monokai`, `nord`, and
`oled` are legacy theme names mapped to current theme/skin pairs. Do not follow
stale `data-theme`-only guidance without first proving the current
+6 -4
View File
@@ -1201,9 +1201,10 @@ const _SKINS=[
{name:'Sienna', colors:['#D97757','#C06A49','#9A523A']},
{name:'Catppuccin',colors:['#CBA6F7','#B4BEFE','#8839EF']},
{name:'Nous', colors:['#4682B4','#3A6E9A','#2C5F88']},
{name:'Geist Contrast', value:'geist-contrast', colors:['#000000','#ffffff','#FFF175']},
];
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.value||s.name).toLowerCase()));
const _LEGACY_THEME_MAP={
slate:{theme:'dark',skin:'slate'},
solarized:{theme:'dark',skin:'poseidon'},
@@ -1365,15 +1366,16 @@ function _buildSkinPicker(activeSkin){
if(!grid) return;
grid.innerHTML='';
for(const skin of _SKINS){
const key=skin.name.toLowerCase();
const key=(skin.value||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);
btn.onclick=()=>_pickSkin(key);
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>`;
const label=skin.label||skin.name;
btn.innerHTML=`<div style="display:flex;gap:3px;justify-content:center;margin-bottom:4px">${dots}</div><span style="font-size:11px;color:var(--text)">${label}</span>`;
grid.appendChild(btn);
}
_syncSkinPicker((activeSkin||'default').toLowerCase());
+11 -11
View File
@@ -188,7 +188,7 @@ const LOCALES = {
cmd_terminal: 'Open the workspace terminal',
cmd_new: 'Start a new chat session',
cmd_usage: 'Toggle token usage display on/off',
cmd_theme: 'Switch appearance (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous)',
cmd_theme: 'Switch appearance (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_personality: 'Switch agent personality',
cmd_skills: 'List available Hermes skills',
available_commands: 'Available commands:',
@@ -1412,7 +1412,7 @@ const LOCALES = {
cmd_terminal: 'Apri il terminale del workspace',
cmd_new: 'Avvia una nuova sessione di chat',
cmd_usage: 'Attiva/disattiva visualizzazione uso token',
cmd_theme: 'Cambia aspetto (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous)',
cmd_theme: 'Cambia aspetto (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_personality: "Cambia personalità dell'agente",
cmd_skills: 'Elenca le skill Hermes disponibili',
available_commands: 'Comandi disponibili:',
@@ -2628,7 +2628,7 @@ const LOCALES = {
cmd_terminal: 'ワークスペースのターミナルを開く',
cmd_new: '新しいチャットセッションを開始',
cmd_usage: 'トークン使用量表示の ON/OFF を切り替え',
cmd_theme: '外観を切り替え (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous)',
cmd_theme: '外観を切り替え (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_personality: 'エージェントのパーソナリティを切り替え',
cmd_skills: '利用可能な Hermes スキルを一覧表示',
available_commands: '利用可能なコマンド:',
@@ -3813,7 +3813,7 @@ const LOCALES = {
cmd_terminal: 'Открыть терминал рабочей области',
cmd_new: 'Начать новую сессию чата',
cmd_usage: 'Показать или скрыть использование токенов',
cmd_theme: 'Переключить внешний вид (тема: system/dark/light, скин: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous)',
cmd_theme: 'Переключить внешний вид (тема: system/dark/light, скин: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_personality: 'Переключить личность агента',
cmd_skills: 'Показать доступные навыки Hermes',
available_commands: 'Доступные команды:',
@@ -4996,7 +4996,7 @@ const LOCALES = {
cmd_terminal: 'Abrir terminal del espacio de trabajo',
cmd_new: 'Iniciar una nueva sesión de chat',
cmd_usage: 'Activar o desactivar el uso de tokens',
cmd_theme: 'Cambiar apariencia (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous)',
cmd_theme: 'Cambiar apariencia (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_personality: 'Cambiar la personalidad del agente',
cmd_skills: 'Listar las skills de Hermes disponibles',
available_commands: 'Comandos disponibles:',
@@ -6125,7 +6125,7 @@ const LOCALES = {
cmd_terminal: 'Workspace-Terminal öffnen',
cmd_new: 'Neue Chat-Sitzung starten',
cmd_usage: 'Token-Verbrauchsanzeige umschalten',
cmd_theme: 'Darstellung wechseln (Theme: system/dark/light, Skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous)',
cmd_theme: 'Darstellung wechseln (Theme: system/dark/light, Skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_personality: 'Agenten-Persönlichkeit wechseln',
cmd_skills: 'Verfügbare Hermes-Skills auflisten',
available_commands: 'Verfügbare Befehle:',
@@ -7305,7 +7305,7 @@ const LOCALES = {
cmd_terminal: '打开工作区 Terminal',
cmd_new: '新建聊天会话',
cmd_usage: '切换 token 用量显示',
cmd_theme: '切换外观(主题:system/dark/light,皮肤:default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous',
cmd_theme: '切换外观(主题:system/dark/light,皮肤:default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast',
cmd_personality: '切换 Agent 人设',
cmd_skills: '列出可用的 Hermes 技能',
available_commands: '可用命令:',
@@ -8422,7 +8422,7 @@ const LOCALES = {
cmd_terminal: '\u6253\u958b\u5de5\u4f5c\u5340 Terminal',
cmd_new: '\u65b0\u5efa\u804a\u5929\u6703\u8a71',
cmd_usage: '\u5207\u63db token \u7528\u91cf\u986f\u793a',
cmd_theme: '\u5207\u63db\u5916\u89c0\uff08\u4e3b\u984c\uff1asystem/dark/light\uff0c\u76ae\u819a\uff1adefault/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous\uff09',
cmd_theme: '\u5207\u63db\u5916\u89c0\uff08\u4e3b\u984c\uff1asystem/dark/light\uff0c\u76ae\u819a\uff1adefault/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast\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',
@@ -9628,7 +9628,7 @@ const LOCALES = {
cmd_workspace: 'Trocar workspace por nome',
cmd_new: 'Iniciar nova sessão de chat',
cmd_usage: 'Alternar exibição de uso de tokens',
cmd_theme: 'Trocar aparência (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous)',
cmd_theme: 'Trocar aparência (tema: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_personality: 'Trocar personalidade do agente',
cmd_skills: 'Listar skills disponíveis do Hermes',
available_commands: 'Comandos disponíveis:',
@@ -10730,7 +10730,7 @@ const LOCALES = {
cmd_terminal: '워크스페이스 터미널 열기',
cmd_new: '새 채팅 세션 시작',
cmd_usage: '토큰 사용량 표시 켜기/끄기',
cmd_theme: 'Switch appearance (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous)',
cmd_theme: 'Switch appearance (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_personality: 'Switch agent personality',
cmd_skills: 'List available Hermes skills',
available_commands: '사용 가능한 명령:',
@@ -11938,7 +11938,7 @@ const LOCALES = {
cmd_terminal: 'Ouvrez le terminal de l\'espace de travail',
cmd_new: 'Démarrer une nouvelle session de discussion',
cmd_usage: 'Activer/désactiver l\'affichage de l\'utilisation du jeton',
cmd_theme: 'Changer d\'apparence (thème : system/dark/light, skin : default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous)',
cmd_theme: 'Changer d\'apparence (thème : system/dark/light, skin : default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)',
cmd_personality: 'Personnalité de l\'agent de commutation',
cmd_skills: 'Lister les compétences Hermès disponibles',
available_commands: 'Commandes disponibles :',
+1 -1
View File
@@ -17,7 +17,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Hermes">
<link rel="apple-touch-icon" sizes="512x512" href="static/apple-touch-icon.png">
<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,catppuccin:1,nous: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,catppuccin:1,nous:1,'geist-contrast':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>
<!-- theme-color: surfaces the active app chrome color to native status bars (Safari status bar, PWA, native WKWebView wrappers). Updated dynamically by boot.js when theme/skin changes. The light/dark default values match style.css :root --sidebar / :root.dark --sidebar. -->
<meta name="theme-color" content="#FAF7F0" media="(prefers-color-scheme: light)">
+167
View File
@@ -297,6 +297,173 @@
:root.dark[data-skin="nous"] .mcp-http{color:#7EB6E0;}
:root.dark[data-skin="nous"] .mcp-status-active{color:#7EB6E0;}
/* Skin: Geist Contrast (Geist-inspired neutral precision)
Dark mode uses a #FFF175 accent; solid accent controls use black
foreground, while selected/navigation states stay neutral and restrained. */
:root[data-skin="geist-contrast"]{
color-scheme:light;
--bg:#ffffff;--sidebar:#fafafa;--surface:#ffffff;--surface-subtle:#fafafa;--surface-subtle-hover:#f5f5f5;
--main-bg:#ffffff;--topbar-bg:rgba(255,255,255,.92);
--border:#eaeaea;--border2:#d4d4d4;--border-subtle:#ededed;--border-muted:#d4d4d4;
--text:#111111;--strong:#000000;--muted:#666666;--em:#444444;
--accent:#0070f3;--accent-hover:#005bd1;--accent-bg:rgba(0,112,243,.075);--accent-bg-strong:rgba(0,112,243,.16);--accent-text:#005bd1;
--blue:#0070f3;--gold:#111111;--focus-ring:rgba(0,112,243,.28);--focus-glow:rgba(0,112,243,.08);
--input-bg:#ffffff;--hover-bg:#f5f5f5;--code-bg:#fafafa;--code-inline-bg:#f5f5f5;--code-text:#111111;--pre-text:#111111;
--error:#e5484d;--success:#007a45;--warning:#b45309;--info:#0070f3;
--radius-sm:4px;--radius-md:6px;--radius-card:8px;--radius-lg:10px;
--font-ui:"Geist","Geist Sans",-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
}
:root.dark[data-skin="geist-contrast"]{
color-scheme:dark;
--bg:#000000;--sidebar:#050505;--surface:#0a0a0a;--surface-subtle:#111111;--surface-subtle-hover:#171717;
--main-bg:#000000;--topbar-bg:rgba(0,0,0,.88);
--border:#262626;--border2:#3f3f3f;--border-subtle:#171717;--border-muted:#2a2a2a;
--text:#ededed;--strong:#ffffff;--muted:#a1a1a1;--em:#d4d4d4;
--accent:#FFF175;--accent-hover:#fff7a8;--accent-bg:rgba(255,241,117,.075);--accent-bg-strong:rgba(255,241,117,.14);--accent-text:#f5e65f;
--blue:#FFF175;--gold:#FFF175;--focus-ring:rgba(255,241,117,.34);--focus-glow:rgba(255,241,117,.10);
--input-bg:#0a0a0a;--hover-bg:#111111;--code-bg:#0a0a0a;--code-inline-bg:#171717;--code-text:#f5f5f5;--pre-text:#ededed;
--error:#ff6369;--success:#3dd68c;--warning:#f5a524;--info:#FFF175;
}
:root[data-skin="geist-contrast"],
:root[data-skin="geist-contrast"] body{font-family:var(--font-ui)!important;letter-spacing:-0.011em;background:var(--bg)!important;}
:root[data-skin="geist-contrast"] .app-titlebar,
:root[data-skin="geist-contrast"] .rail,
:root[data-skin="geist-contrast"] .sidebar,
:root[data-skin="geist-contrast"] .rightpanel,
:root[data-skin="geist-contrast"] .topbar,
:root[data-skin="geist-contrast"] .composer-wrap{background:var(--sidebar)!important;border-color:var(--border)!important;box-shadow:none!important;backdrop-filter:saturate(180%) blur(12px);}
:root[data-skin="geist-contrast"] .main,
:root[data-skin="geist-contrast"] .chat,
:root[data-skin="geist-contrast"] .empty-state,
:root[data-skin="geist-contrast"] .panel-view,
:root[data-skin="geist-contrast"] .settings-section,
:root[data-skin="geist-contrast"] .settings-card,
:root[data-skin="geist-contrast"] .model-dropdown,
:root[data-skin="geist-contrast"] .profile-menu,
:root[data-skin="geist-contrast"] .session-actions-menu,
:root[data-skin="geist-contrast"] .app-dialog,
:root[data-skin="geist-contrast"] .kanban-modal,
:root[data-skin="geist-contrast"] .cron-item,
:root[data-skin="geist-contrast"] .tool-card,
:root[data-skin="geist-contrast"] .msg-body pre,
:root[data-skin="geist-contrast"] .preview-md pre,
:root[data-skin="geist-contrast"] .composer-box{background:var(--surface)!important;border-color:var(--border)!important;box-shadow:none!important;}
:root[data-skin="geist-contrast"] .session-item,
:root[data-skin="geist-contrast"] .nav-tab,
:root[data-skin="geist-contrast"] .rail-btn,
:root[data-skin="geist-contrast"] .icon-btn,
:root[data-skin="geist-contrast"] .panel-head-btn,
:root[data-skin="geist-contrast"] .sm-btn,
:root[data-skin="geist-contrast"] .btn,
:root[data-skin="geist-contrast"] button,
:root[data-skin="geist-contrast"] select,
:root[data-skin="geist-contrast"] input,
:root[data-skin="geist-contrast"] textarea,
:root[data-skin="geist-contrast"] .chip,
:root[data-skin="geist-contrast"] .project-chip,
:root[data-skin="geist-contrast"] .profile-chip,
:root[data-skin="geist-contrast"] .model-chip,
:root[data-skin="geist-contrast"] .reasoning-chip,
:root[data-skin="geist-contrast"] .app-dialog-btn{border-radius:6px!important;}
:root[data-skin="geist-contrast"] .session-item:hover,
:root[data-skin="geist-contrast"] .nav-tab:hover,
:root[data-skin="geist-contrast"] .rail-btn:hover,
:root[data-skin="geist-contrast"] .icon-btn:hover,
:root[data-skin="geist-contrast"] .panel-head-btn:hover,
:root[data-skin="geist-contrast"] .sm-btn:hover,
:root[data-skin="geist-contrast"] .project-chip:hover,
:root[data-skin="geist-contrast"] .chip:hover,
:root[data-skin="geist-contrast"] .profile-opt:hover,
:root[data-skin="geist-contrast"] .ws-opt:hover,
:root[data-skin="geist-contrast"] .file-item:hover,
:root[data-skin="geist-contrast"] .suggestion:hover{background:var(--hover-bg)!important;}
:root[data-skin="geist-contrast"] .session-item.active,
:root[data-skin="geist-contrast"] .nav-tab.active,
:root[data-skin="geist-contrast"] .rail-btn.active,
:root[data-skin="geist-contrast"] .project-chip.active,
:root[data-skin="geist-contrast"] .profile-opt.active,
:root[data-skin="geist-contrast"] .chip.model,
:root[data-skin="geist-contrast"] .side-menu-item.active,
:root[data-skin="geist-contrast"] .skin-pick-btn.active,
:root[data-skin="geist-contrast"] .theme-pick-btn.active,
:root[data-skin="geist-contrast"] .font-size-pick-btn.active{background:var(--surface-subtle)!important;border-color:var(--border2)!important;color:var(--text)!important;box-shadow:inset 0 0 0 1px var(--border)!important;font-weight:500!important;}
:root[data-skin="geist-contrast"] .session-item.active{position:relative;border:1px solid var(--border2)!important;}
:root[data-skin="geist-contrast"] .session-item.active::before{content:"";position:absolute;left:6px;top:10px;bottom:10px;width:2px;border-radius:999px;background:var(--accent);}
:root[data-skin="geist-contrast"] .session-item.active .session-title,
:root[data-skin="geist-contrast"] .session-item.active .session-meta,
:root[data-skin="geist-contrast"] .session-item.active .session-preview,
:root[data-skin="geist-contrast"] .session-item.active *{color:inherit!important;}
:root.dark[data-skin="geist-contrast"] .session-item.active,
:root.dark[data-skin="geist-contrast"] .session-item.active *{color:var(--text)!important;}
:root[data-skin="geist-contrast"] .nav-tab.active,
:root[data-skin="geist-contrast"] .rail-btn.active{color:var(--accent-text)!important;}
:root.dark[data-skin="geist-contrast"] .nav-tab.active svg,
:root.dark[data-skin="geist-contrast"] .rail-btn.active svg,
:root.dark[data-skin="geist-contrast"] .nav-tab.active [data-lucide],
:root.dark[data-skin="geist-contrast"] .rail-btn.active [data-lucide]{color:var(--accent-text)!important;stroke:var(--accent-text)!important;}
:root[data-skin="geist-contrast"] .profile-chip,
:root[data-skin="geist-contrast"] .model-chip,
:root[data-skin="geist-contrast"] .reasoning-chip,
:root[data-skin="geist-contrast"] .composer-workspace-chip,
:root[data-skin="geist-contrast"] .composer-profile-chip,
:root[data-skin="geist-contrast"] .composer-model-chip{color:var(--muted)!important;border-color:transparent!important;background:transparent!important;}
:root[data-skin="geist-contrast"] .profile-chip:hover,
:root[data-skin="geist-contrast"] .model-chip:hover,
:root[data-skin="geist-contrast"] .reasoning-chip:hover,
:root[data-skin="geist-contrast"] .composer-workspace-chip:hover,
:root[data-skin="geist-contrast"] .composer-profile-chip:hover,
:root[data-skin="geist-contrast"] .composer-model-chip:hover{color:var(--text)!important;background:var(--surface-subtle)!important;border-color:var(--border)!important;}
:root[data-skin="geist-contrast"] .new-chat-btn,
:root[data-skin="geist-contrast"] button.send-btn:not(:disabled),
:root[data-skin="geist-contrast"] .btn.primary,
:root[data-skin="geist-contrast"] .update-primary,
:root[data-skin="geist-contrast"] .app-dialog-btn.confirm,
:root[data-skin="geist-contrast"] .approval-btn.once,
:root[data-skin="geist-contrast"] .approval-btn.session,
:root[data-skin="geist-contrast"] .clarify-submit{background:var(--accent)!important;border-color:var(--accent)!important;color:#050505!important;font-weight:600!important;box-shadow:none!important;}
:root[data-skin="geist-contrast"]:not(.dark) .new-chat-btn,
:root[data-skin="geist-contrast"]:not(.dark) button.send-btn:not(:disabled),
:root[data-skin="geist-contrast"]:not(.dark) .btn.primary,
:root[data-skin="geist-contrast"]:not(.dark) .update-primary,
:root[data-skin="geist-contrast"]:not(.dark) .app-dialog-btn.confirm,
:root[data-skin="geist-contrast"]:not(.dark) .approval-btn.once,
:root[data-skin="geist-contrast"]:not(.dark) .approval-btn.session,
:root[data-skin="geist-contrast"]:not(.dark) .clarify-submit{color:#ffffff!important;}
:root[data-skin="geist-contrast"] .new-chat-btn:hover,
:root[data-skin="geist-contrast"] button.send-btn:not(:disabled):hover,
:root[data-skin="geist-contrast"] .btn.primary:hover,
:root[data-skin="geist-contrast"] .update-primary:hover,
:root[data-skin="geist-contrast"] .app-dialog-btn.confirm:hover,
:root[data-skin="geist-contrast"] .approval-btn.once:hover,
:root[data-skin="geist-contrast"] .approval-btn.session:hover,
:root[data-skin="geist-contrast"] .clarify-submit:hover{background:var(--accent-hover)!important;border-color:var(--accent-hover)!important;transform:none!important;}
:root[data-skin="geist-contrast"] button.send-btn:disabled{background:var(--surface-subtle)!important;border-color:var(--border)!important;color:var(--muted)!important;opacity:1!important;}
:root.dark[data-skin="geist-contrast"] button.send-btn:disabled svg,
:root.dark[data-skin="geist-contrast"] button.send-btn:disabled [data-lucide]{color:var(--muted)!important;stroke:var(--muted)!important;}
:root[data-skin="geist-contrast"] input:focus,
:root[data-skin="geist-contrast"] textarea:focus,
:root[data-skin="geist-contrast"] select:focus,
:root[data-skin="geist-contrast"] .composer-box:focus-within,
:root[data-skin="geist-contrast"] .app-dialog-input:focus,
:root[data-skin="geist-contrast"] .sidebar-search input:focus{border-color:var(--accent)!important;box-shadow:0 0 0 3px var(--focus-ring)!important;outline:none!important;}
:root[data-skin="geist-contrast"] .logo,
:root[data-skin="geist-contrast"] .sidebar-header .logo{background:var(--surface-subtle)!important;color:var(--accent-text)!important;border:1px solid var(--border2)!important;border-radius:8px!important;box-shadow:none!important;font-weight:650!important;}
:root[data-skin="geist-contrast"] .app-titlebar-icon rect,
:root[data-skin="geist-contrast"] .app-titlebar-icon path,
:root[data-skin="geist-contrast"] .app-titlebar-icon circle{color:var(--accent-text)!important;fill:var(--accent-text)!important;}
:root[data-skin="geist-contrast"] .msg-row[data-role="user"] .msg-body{background:var(--surface-subtle)!important;border:1px solid var(--border)!important;border-radius:10px!important;}
:root[data-skin="geist-contrast"] .suggestion{background:var(--surface)!important;border-color:var(--border)!important;color:var(--muted)!important;transition:background .15s ease,border-color .15s ease,color .15s ease!important;}
:root[data-skin="geist-contrast"] .suggestion:hover{border-color:var(--border2)!important;color:var(--text)!important;}
:root[data-skin="geist-contrast"] .msg-body a,
:root[data-skin="geist-contrast"] .preview-md a,
:root[data-skin="geist-contrast"] .empty-state a,
:root[data-skin="geist-contrast"] .tool-arg-key,
:root[data-skin="geist-contrast"] .tool-card-more,
:root[data-skin="geist-contrast"] .session-pin-indicator,
:root[data-skin="geist-contrast"] .sidebar-date-header.pinned{color:var(--accent-text)!important;}
:root[data-skin="geist-contrast"]::-webkit-scrollbar-thumb{background:var(--border2)!important;}
:root[data-skin="geist-contrast"] ::selection{background:var(--accent-bg-strong);color:var(--strong);}
/* #594: app-dialog light mode overrides — base styles use hardcoded dark gradients */
:root:not(.dark) .app-dialog{
background:linear-gradient(180deg,rgba(240,237,232,.99),rgba(228,224,216,.99));
+47
View File
@@ -0,0 +1,47 @@
"""Geist Contrast skin registration and contrast affordances."""
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")
CONFIG_PY = (REPO / "api" / "config.py").read_text(encoding="utf-8")
INDEX_HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8")
def test_geist_contrast_skin_is_registered_with_matching_key_and_label():
assert "{name:'Geist Contrast'" in BOOT_JS
assert "value:'geist-contrast'" in BOOT_JS
assert "s.value||s.name" in BOOT_JS
assert "'geist-contrast':1" in INDEX_HTML
assert '"geist-contrast"' in CONFIG_PY
def test_geist_contrast_dark_tokens_use_yellow_accent_with_neutral_surfaces():
assert ':root.dark[data-skin="geist-contrast"]' in CSS
assert "--bg:#000000" in CSS
assert "--surface:#0a0a0a" in CSS
assert "--text:#ededed" in CSS
assert "--accent:#FFF175" in CSS
assert "--accent-text:#f5e65f" in CSS
def test_geist_contrast_selection_is_neutral_not_solid_yellow():
active_rule = ':root[data-skin="geist-contrast"] .session-item.active{position:relative;border:1px solid var(--border2)!important;}'
marker_rule = ':root[data-skin="geist-contrast"] .session-item.active::before{content:"";position:absolute;left:6px;top:10px;bottom:10px;width:2px;border-radius:999px;background:var(--accent);}'
dark_text_rule = ':root.dark[data-skin="geist-contrast"] .session-item.active,\n :root.dark[data-skin="geist-contrast"] .session-item.active *{color:var(--text)!important;}'
assert active_rule in CSS
assert marker_rule in CSS
assert dark_text_rule in CSS
def test_geist_contrast_solid_accent_controls_use_black_text_in_dark_mode():
assert ':root[data-skin="geist-contrast"] button.send-btn:not(:disabled)' in CSS
assert "color:#050505!important" in CSS
assert ':root[data-skin="geist-contrast"] button.send-btn:disabled{background:var(--surface-subtle)!important;border-color:var(--border)!important;color:var(--muted)!important;opacity:1!important;}' in CSS
assert ':root.dark[data-skin="geist-contrast"] button.send-btn:disabled svg' in CSS
def test_geist_contrast_composer_chips_are_neutral_until_hovered():
neutral_rule = ':root[data-skin="geist-contrast"] .profile-chip,\n :root[data-skin="geist-contrast"] .model-chip,\n :root[data-skin="geist-contrast"] .reasoning-chip,\n :root[data-skin="geist-contrast"] .composer-workspace-chip,\n :root[data-skin="geist-contrast"] .composer-profile-chip,\n :root[data-skin="geist-contrast"] .composer-model-chip{color:var(--muted)!important;border-color:transparent!important;background:transparent!important;}'
assert neutral_rule in CSS
+1 -1
View File
@@ -29,7 +29,7 @@ def test_theme_command_help_mentions_current_theme_and_skin_values():
"""Every /theme help string should describe the current Theme × Skin contract."""
required_fragments = (
"system/dark/light",
"default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous",
"default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast",
)
for locale in ("en", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "fr"):
value = _literal_value(_locale_block(locale), "cmd_theme")