From 4598adfd04df2fa5928c3eb1cd45774cfaf2a311 Mon Sep 17 00:00:00 2001 From: Eleanor Berger Date: Mon, 18 May 2026 13:10:17 +0200 Subject: [PATCH] feat: add Geist Contrast skin --- CHANGELOG.md | 7 ++ README.md | 2 +- THEMES.md | 7 +- api/config.py | 1 + docs/UIUX-GUIDE.md | 2 +- static/boot.js | 10 +- static/i18n.js | 22 ++-- static/index.html | 2 +- static/style.css | 167 +++++++++++++++++++++++++++++ tests/test_geist_contrast_skin.py | 47 ++++++++ tests/test_issue2462_theme_i18n.py | 2 +- 11 files changed, 247 insertions(+), 22 deletions(-) create mode 100644 tests/test_geist_contrast_skin.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 20fd6277..976f0104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 9086bd21..e2d50a7c 100644 --- a/README.md +++ b/README.md @@ -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 ` - 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 diff --git a/THEMES.md b/THEMES.md index 017d8a9f..12937308 100644 --- a/THEMES.md +++ b/THEMES.md @@ -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 ` 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=""` on `` (the default diff --git a/api/config.py b/api/config.py index a5841547..b75e4adb 100644 --- a/api/config.py +++ b/api/config.py @@ -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. diff --git a/docs/UIUX-GUIDE.md b/docs/UIUX-GUIDE.md index 7f9edc4e..78a728f3 100644 --- a/docs/UIUX-GUIDE.md +++ b/docs/UIUX-GUIDE.md @@ -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 diff --git a/static/boot.js b/static/boot.js index 9a30fe8e..24fd34be 100644 --- a/static/boot.js +++ b/static/boot.js @@ -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=>``).join(''); - btn.innerHTML=`
${dots}
${skin.name}`; + const label=skin.label||skin.name; + btn.innerHTML=`
${dots}
${label}`; grid.appendChild(btn); } _syncSkinPicker((activeSkin||'default').toLowerCase()); diff --git a/static/i18n.js b/static/i18n.js index ee4f365f..8a5f77ff 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -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 :', diff --git a/static/index.html b/static/index.html index 5f1cca5a..fa865855 100644 --- a/static/index.html +++ b/static/index.html @@ -17,7 +17,7 @@ - + diff --git a/static/style.css b/static/style.css index 1c5ad2e9..8a84a24d 100644 --- a/static/style.css +++ b/static/style.css @@ -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)); diff --git a/tests/test_geist_contrast_skin.py b/tests/test_geist_contrast_skin.py new file mode 100644 index 00000000..d4a0e143 --- /dev/null +++ b/tests/test_geist_contrast_skin.py @@ -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 diff --git a/tests/test_issue2462_theme_i18n.py b/tests/test_issue2462_theme_i18n.py index c34bea37..07e77302 100644 --- a/tests/test_issue2462_theme_i18n.py +++ b/tests/test_issue2462_theme_i18n.py @@ -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")