## Release v0.51.242 — Release HJ (stage-q14) UX-approved via Telegram (Nathan — dark/light/mobile screenshots). ### Added | PR | Author | Feature | |----|--------|---------| | #3440 | @t3chn0pr13st | **Graphite appearance skin** — a quiet, neutral-gray "workbench" alternative to the default gold/cream. Selectable from Settings → Appearance and `/theme skin graphite`. Both light + dark palettes on the existing CSS-variable token system; tightened typography, shadows, active-sidebar spacing, code-block framing. | ### Why it's safe - **Fully scoped + additive**: every new CSS rule (and every `!important`) is under `[data-skin="graphite"]` — Codex verified zero bleed into the default appearance or other skins. `api/config.py` keeps the default skin as `default` and only *adds* `graphite` to the allowed set. No i18n keys dropped (only the `/theme` help string gains `graphite`). - Opt-in; a user has to select it. Default experience unchanged. ### Test-robustness fix (absorbed) The new graphite scoped selectors (e.g. `:root[data-skin="graphite"] .session-item.active .session-time{…}`) appear in `style.css` *before* the canonical unscoped rules, which broke 3 naive first-occurrence CSS-contract tests (`test_issue677` scroll-btn-overlay, `test_issue856_pinned_indicator_layout`, `test_workspace_panel_session_list`). Fixed those 3 to anchor on the canonical **unscoped** rule (start-of-line regex) instead of the first `.selector` match — robust against this and future skins. Verified they still pass on clean master CSS (invariant preserved, not weakened). ### Gate - Full pytest suite: **7517 passed, 9 skipped, 3 xpassed, 0 failed** - ESLint runtime gate: CLEAN · ruff: CLEAN · browser-smoke: CLEAN - Codex (regression): **SAFE TO SHIP** (verified all CSS scoped, default skin unchanged, no i18n key loss) - Vision-verified dark + light + mobile; UX-approved by Nathan via Telegram Co-authored-by: t3chn0pr13st <t3chn0pr13st@users.noreply.github.com>
44 lines
1.7 KiB
Python
44 lines
1.7 KiB
Python
"""Regression coverage for #2462 stale /theme i18n help strings."""
|
||
|
||
from pathlib import Path
|
||
import re
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
||
|
||
|
||
def _locale_block(locale: str) -> str:
|
||
# Locale keys are mostly bare identifiers, but zh-Hant is quoted. Match the
|
||
# requested block up to the next top-level locale block or the LOCALES close.
|
||
match = re.search(
|
||
rf"\n\s*['\"]?{re.escape(locale)}['\"]?:\s*\{{(?P<body>.*?)(?=\n\s*['\"]?[a-z][\w-]*['\"]?:\s*\{{|\n\}};)",
|
||
I18N_JS,
|
||
re.S,
|
||
)
|
||
assert match, f"locale block {locale!r} not found"
|
||
return match.group("body")
|
||
|
||
|
||
def _literal_value(block: str, key: str) -> str:
|
||
match = re.search(rf"\n\s*{re.escape(key)}:\s*'(?P<value>(?:\\'|[^'])*)',", block)
|
||
assert match, f"{key!r} not found in locale block"
|
||
return match.group("value")
|
||
|
||
|
||
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/graphite/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast",
|
||
)
|
||
for locale in ("en", "it", "ja", "ru", "es", "de", "zh", "zh-Hant", "pt", "ko", "fr", "tr"):
|
||
value = _literal_value(_locale_block(locale), "cmd_theme")
|
||
for fragment in required_fragments:
|
||
assert fragment in value, f"{locale} cmd_theme missing {fragment!r}: {value!r}"
|
||
|
||
|
||
def test_french_theme_usage_uses_actual_slash_command_with_space():
|
||
fr_theme_usage = _literal_value(_locale_block("fr"), "theme_usage")
|
||
assert fr_theme_usage == "Utilisation : /theme "
|
||
assert "/thème" not in fr_theme_usage
|