fix(theme): --ot-* 变量名 camelCase/kebab-case 不匹配导致浅色分支静默失效

CSS 自定义属性名大小写敏感,而 semanticVars() 的键是 camelCase、样式表读的是
kebab-case:chrome.ts 直接写 `--ot-${key}` 时,9 个多词变量(--ot-text-muted、
--ot-text-secondary、--ot-surface-raised、--ot-border-strong、--ot-success-fg、
--ot-danger-fg、--ot-warning-fg、--ot-info-fg、--ot-scrollbar-active、
--ot-focus-ring)从未真正生效,元素一律回落到 global.css :root 的深色兜底值——
浅色主题正是靠这些变量修可读性的,等于白改(--ot-text-muted 48 处引用仍是
#8a8a8a,白底 3.25:1;--ot-danger-fg 仍是 #f85149,3.15:1)。

- 键 `muted` 改名 `textMuted`(CSS 读的是 --ot-text-muted)
- 新增 semantic.ts 的 cssVarName()/semanticCssVars(),chrome.ts 只调用它们,
  命名逻辑单一来源
- 新增 tests/ot-var-names.mjs:把「CSS 引用名 = JS 写入名 = :root 声明名」
  三集合锁在一起(值测试看不出这类问题),已用「去掉转换」验证会失败
- AGENTS/STATE 同步该铁律;测试数 22/25 的陈旧计数修正为实际的 29
This commit is contained in:
Bill committed 2026-10-16 23:29:03 +08:00
1 parent 9e424138b5
commit 3b86d62111
7 files changed
+120 -17

No files matched your search

+6 -7
View File
@@ -1,5 +1,5 @@
import type { ThemeColors } from '@shared/theme'
import { contrast, isLightTheme, mix, semanticVars } from './semantic'
import { contrast, isLightTheme, mix, semanticCssVars } from './semantic'
// 兼容既有导入方(main.tsx 的 mix);新代码请直接从 ./semantic 导入。
export { contrast, luminance, mix } from './semantic'
@@ -60,12 +60,11 @@ export function applyChromeTheme(colors: ThemeColors): void {
// styles. The dark branch locks the SEMANTIC anchors (zero visual change);
// the light branch darkens them until the WCAG ratio holds against this
// theme's background. `color-scheme` rides along so native scrollbars and
// form controls follow the same switch.
const vars = semanticVars(colors, chromeIsLight)
for (const [key, value] of Object.entries(vars)) {
if (key === 'colorScheme') style.setProperty('color-scheme', value)
else set(`--ot-${key}`, value)
}
// form controls follow the same switch. semanticCssVars() owns the camelCase
// -> kebab-case naming (custom property names are case-sensitive, so a miss
// would leave the variable unset and silently fall back to the dark :root
// literal); tests/ot-var-names.mjs guards it.
for (const [name, value] of Object.entries(semanticCssVars(colors, chromeIsLight))) set(name, value)
applyTabAccent(tabAccent)
}
+21 -3
View File
@@ -88,7 +88,8 @@ export const SEMANTIC = {
focusRing: 'rgba(59, 142, 234, 0.2)'
} as const
/** Values for --ot-* variables (keys without the prefix). */
/** Values for --ot-* variables (camelCase keys; chrome.ts converts them to
* kebab-case, so `textMuted` lands as `--ot-text-muted`). */
export function semanticVars(colors: ThemeLike, light: boolean): Record<string, string> {
const bg = colors.background
const fg = colors.foreground
@@ -96,7 +97,7 @@ export function semanticVars(colors: ThemeLike, light: boolean): Record<string,
return {
text: fg,
textSecondary: mix(bg, fg, 0.78) ?? fg,
muted: SEMANTIC.muted,
textMuted: SEMANTIC.muted,
surface: mix(bg, fg, 0.05) ?? bg,
surfaceRaised: SEMANTIC.surfaceRaised,
hover: SEMANTIC.hover,
@@ -120,7 +121,7 @@ export function semanticVars(colors: ThemeLike, light: boolean): Record<string,
return {
text: darkenToRatio(fg, bg, 7),
textSecondary: darkenToRatio(mix(bg, fg, 0.75) ?? fg, bg, 4.5),
muted: darkenToRatio(SEMANTIC.muted, bg, 4.5),
textMuted: darkenToRatio(SEMANTIC.muted, bg, 4.5),
surface: mix(bg, fg, 0.04) ?? bg,
surfaceRaised: mix(bg, '#000000', 0.045) ?? bg,
hover: 'rgba(0, 0, 0, 0.06)',
@@ -141,3 +142,20 @@ export function semanticVars(colors: ThemeLike, light: boolean): Record<string,
colorScheme: 'light'
}
}
/** Final CSS custom property name for a semanticVars() key. Custom property
* names are case-sensitive, so this kebab conversion is the only thing tying
* the camelCase keys to the --ot-* names the stylesheets read — a miss leaves
* the variable unset and the element falls back to the dark :root literal.
* Exported so chrome.ts and tests/ot-var-names.mjs share one implementation. */
export function cssVarName(key: string): string {
if (key === 'colorScheme') return 'color-scheme'
return `--ot-${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`
}
/** semanticVars() keyed by their final CSS custom property names. */
export function semanticCssVars(colors: ThemeLike, light: boolean): Record<string, string> {
const out: Record<string, string> = {}
for (const [key, value] of Object.entries(semanticVars(colors, light))) out[cssVarName(key)] = value
return out
}