mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 03:00:23 +00:00
feat: add Geist Contrast skin
This commit is contained in:
committed by
nesquena-hermes
parent
c8896ac1b6
commit
4598adfd04
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)">
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user