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

+3 -3
View File
@@ -7,10 +7,10 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
- 开发:`npm run dev`(主进程改动不热重建,需重启)
- dev 实例使用独立用户数据目录 `%APPDATA%\OpenTerminal-dev` 与独立单实例锁(`src/main/index.ts` 顶部 `!app.isPackaged` 分支),窗口标题带 `(dev)`:**可与已安装的正式版同时运行,互不干扰**,也不会把测试设置/会话写进真实配置
- 类型检查:`npm run typecheck`(tsconfig.node.json + tsconfig.web.json;只看渲染层可单跑 `npx tsc --noEmit -p tsconfig.web.json`)
- 测试:`npm test`(**npm 生命周期先自动跑 `pretest` 做类型检查**,再 `node tests/build-bundles.cjs` 重建 esbuild bundle,然后依次跑可离线运行的 22 个测试:ssh-loopback、commands-store、connections-store、settings-store、local-path-grants、lock-store、lock-controller、lock-shortcuts、hl-split-smoke、hl-rules、reserved-accelerators、ipc-guard、updater-fallback、log-sanitizer、sftp-timeout、terminal-title、zmodem-e2e、ssh-session-e2e、sysinfo-e2e、terminal-cwd、terminal-links、broadcast-store;真实服务器测试需 JD_* 凭据,不在此列)。后三个渲染层纯函数测试用 Node 22.18+ 的原生 TS 类型剥离**直接 import `.ts` 源文件**(不经 esbuild bundle),Node 20/22.17 以下会 `ERR_UNKNOWN_FILE_EXTENSION`
- 测试:`npm test`(**npm 生命周期先自动跑 `pretest` 做类型检查**,再 `node tests/build-bundles.cjs` 重建 esbuild bundle,然后依次跑可离线运行的 29 个测试:ssh-loopback、commands-store、connections-store、settings-store、local-path-grants、lock-store、lock-controller、lock-shortcuts、reserved-accelerators、terminal-title、ipc-guard、updater-fallback、log-sanitizer、app-log、sftp-timeout、hl-split-smoke、hl-rules、zmodem-e2e、ssh-session-e2e、sysinfo-e2e、terminal-cwd、terminal-links、broadcast-store、write-batcher、refit-throttle、semantic-theme、theme-contrast、css-theme-vars、ot-var-names;真实服务器测试需 JD_* 凭据,不在此列)。其中渲染层纯函数测试(semantic-theme、theme-contrast、ot-var-names、write-batcher、refit-throttle、broadcast-store、terminal-cwd、terminal-links、terminal-title、hl-split-smoke、hl-rules 等)用 Node 22.18+ 的原生 TS 类型剥离**直接 import `.ts` 源文件**(不经 esbuild bundle),Node 20/22.17 以下会 `ERR_UNKNOWN_FILE_EXTENSION`
- 依赖分类规则:**只有 `src/main/`/`src/preload/` 实际 import 的包才能进 `dependencies`**(node-pty/ssh2/zmodem.js/font-list/electron-updater);纯渲染层依赖一律 devDependencies(Vite 全量打包进 out/renderer,`externalizeDepsPlugin` 不作用渲染层)——这条让 asar 从 98MB 瘦到 8.1MB,加新依赖时别放错边
- 下载量统计:`node scripts/download-stats.cjs`(Gitea + GitHub release 资产的 download_count 汇总;GitHub 优先直连、失败自动回退 `HTTPS_PROXY`/本地 7897;更新通道无计数接口不计入)
- 打包:`npm run dist`(**生命周期先自动跑 `predist` → `npm test`,即类型检查 + 22 个离线测试全部通过后才 build/package**,typecheck 全程只跑一次;predist 末尾的 `npm install --package-lock-only` 会把 `package-lock.json` 根版本号对齐 `package.json`,**发布提交必须带上 package-lock.json**),产物在 `release/`(exe + latest.yml + blockmap)
- 打包:`npm run dist`(**生命周期先自动跑 `predist` → `npm test`,即类型检查 + 29 个离线测试全部通过后才 build/package**,typecheck 全程只跑一次;predist 末尾的 `npm install --package-lock-only` 会把 `package-lock.json` 根版本号对齐 `package.json`,**发布提交必须带上 package-lock.json**),产物在 `release/`(exe + latest.yml + blockmap)
- GitHub Actions:`.github/workflows/ci.yml` 在 windows-latest + Node 22 上跑 `npm ci` / `npm test`(含 pretest typecheck)/ `npm run build`,只做验证,不打包安装器、不发布
- 国内网络需镜像:`ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ npm run dist`
- 依赖一致性闸门:`predist` 开头跑 `node scripts/verify-deps.cjs`(逐条比对 `node_modules` 与 `package-lock.json` 的版本,跨平台 optional 依赖缺失跳过;不一致就 exit 1)——`npm install --package-lock-only` **只重算 lockfile、不碰 node_modules**(本版 npm 还会把 `node_modules/.package-lock.json` 一并重写成理想树,制造「已经装好了」的假象),所以**改动依赖版本后必须真实 `npm install` 或 `npm ci` 再构建**,别指望 lockfile 对齐就等于树里换了包;v1.0.21 的中文输入法回归正是这个坑(`@xterm/xterm` 锁 6.1.0-beta.304,树里还是 6.0.0,打进去的是旧代码)
@@ -71,7 +71,7 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
## 主题机制
- 终端主题由 xterm 主题派生 UI 配色:`src/renderer/src/theme/chrome.ts` 的 `applyChromeTheme` 写入 `--chrome-bg/-bg-deep/-border/-hover` CSS 变量,antd token 在 `main.tsx` ThemedConfigProvider 派生
- **语义变量层(2026-10 主题改造)**:`src/renderer/src/theme/semantic.ts`(**零 import 纯模块**——`tests/semantic-theme.mjs` 经 Node 原生类型剥离直连它,加 import 前先想清楚)导出 `isLightTheme`/`mix`/`luminance`/`contrast`/`darkenToRatio`/`SEMANTIC` 锚点;`chrome.ts` 的 `applyChromeTheme` 随主题派生 `--ot-*` 变量并写 `color-scheme: dark|light`(原生滚动条/表单跟随)。**深色分支锁 SEMANTIC 锚点原值(零回归),浅色分支 `darkenToRatio` 按 WCAG 派生**(正文/-fg ≥4.5:1,语义基色 ≥3:1)。CSS 一律 `var(--ot-*, 锚点兜底)`,新代码禁止裸颜色字面量——`node tests/css-theme-vars.mjs` fail-closed 扫描(剥注释/color-mix/var 兜底走受控清单 FALLBACK_OK,跳过含 `${` 的模板串行,白名单按「文件+精确值」登记在测试内);**canvas 绘制吃不到 CSS 变量**(`ctx.strokeStyle` 不解析 var()),图表类组件用 `useResolvedTheme()` + `semanticVars()` 取实色(MonitorPanel 的做法)
- **语义变量层(2026-10 主题改造)**:`src/renderer/src/theme/semantic.ts`(**零 import 纯模块**——`tests/semantic-theme.mjs` 经 Node 原生类型剥离直连它,加 import 前先想清楚)导出 `isLightTheme`/`mix`/`luminance`/`contrast`/`darkenToRatio`/`SEMANTIC` 锚点;`chrome.ts` 的 `applyChromeTheme` 随主题派生 `--ot-*` 变量并写 `color-scheme: dark|light`(原生滚动条/表单跟随)。**深色分支锁 SEMANTIC 锚点原值(零回归),浅色分支 `darkenToRatio` 按 WCAG 派生**(正文/-fg ≥4.5:1,语义基色 ≥3:1)。CSS 一律 `var(--ot-*, 锚点兜底)`,新代码禁止裸颜色字面量——`node tests/css-theme-vars.mjs` fail-closed 扫描(剥注释/color-mix/var 兜底走受控清单 FALLBACK_OK,跳过含 `${` 的模板串行,白名单按「文件+精确值」登记在测试内);**canvas 绘制吃不到 CSS 变量**(`ctx.strokeStyle` 不解析 var()),图表类组件用 `useResolvedTheme()` + `semanticVars()` 取实色(MonitorPanel 的做法)。**变量名大小写敏感**:`semanticVars()` 的键是 camelCase 而 CSS 读的是 kebab-case,`chrome.ts` 必须经 `semanticCssVars()`(内部 `cssVarName()`)转换后再写——直接写 camelCase 名**不报错**,只是该变量永不生效、元素静默回落到 `:root` 的深色兜底值(浅色主题就这么坏过一轮:`--ot-text-muted` 等 9 个多词变量全部没生效)。`node tests/ot-var-names.mjs` 把「CSS 引用名 = JS 写入名 = global.css `:root` 声明名」三个集合锁在一起——**值测试(semantic-theme/theme-contrast)看不出这类问题**,改语义层时别只跑它们
- **浅色主题可读性**:内置浅色主题 ANSI 调色板已重做,16 色+fg 全部 ≥4.5:1(`node tests/theme-contrast.mjs` 守门,改浅色主题必须过它;改色用 `darkenToRatio` 统一加深算法,不许手挑色);`minimumContrastRatio` 按「背景图 4.5|浅色无图 4.5|深色无图 1」矩阵(构造+热更两处都要改),`drawBoldTextInBrightColors` 浅色关闭;WebGL 渲染器同样执行 MCR(addon-webgl 自带 `_getMinimumContrastColor` 对比度路径,已核实);antd 算法随 `isLightTheme` 切 `defaultAlgorithm`/`darkAlgorithm`(**antd 没有 lightAlgorithm,浅色就是默认算法**);github-light 底色已柔化 `#f6f8fa`
- 标签强调色:`settings.tabAccentColor`(默认 `#3fb950`),经 `--tab-accent` CSS 变量生效;`--tab-accent-fg` 是强调底上的文字色(明暗自适应,选中态按钮文字也用它)
- 背景图(`settings.terminal.backgroundImage`):图片层 `.term-bg-image` 在 xterm 画布之下(`terminal.css`),xterm 背景设 `#00000000` 透明。**图片模式下的三条铁律**:① `.terminal-view.has-bg-image .terminal-view-dock` 强制深色底衬 `#0d1117`,不能用 `--chrome-bg`——浅色主题下它是白色,调低透明度会把图洗白而不是压暗;② `minimumContrastRatio` 从 1 提到 4.5(WCAG AA,与 VS Code 终端默认值一致;TerminalView 构造与设置热更两处都要改;注意正确拼写是 `minimumContrastRatio`,`minContrastRatio` 会被静默忽略),xterm 的对比度计算把透明背景当黑色亮度,会把浅色主题的深色前景自动提亮到可读。无图时保持 1 不动主题配色。③ 压暗层(`settings.terminal.backgroundImageDim`,0–90,**默认 0**):`.term-bg-dim` 纯黑 scrim 在 DOM 序上位于 `.term-bg-image` 之后、xterm(z-index:1)之下(同在 z-index:0 层,靠 DOM 序压图),与透明度的区别是透明度把图与深色底衬混合、压暗在图之上叠黑(保饱和但整体变暗);**dim=0 时 TerminalView 不渲染该层,零成本**
+1 -1
View File
@@ -28,7 +28,7 @@
| M11 终端自定义(背景图/压暗层+对比度护栏)、高亮预设扩到 22 条、传输健壮性修复 | ✅(v1.0.14–1.0.16) |
| M12 锁屏(主窗口遮罩+scrypt 密码+Ctrl+L 立即锁屏+冷却阶梯+落盘状态) | ✅(v1.0.17,v1.0.18 修复锁屏输入回归,v1.0.20 摘除菜单加固) |
| M13 更新源改为 GitHub 优先 + Gitea 通道兜底,高亮引擎性能优化 | ✅(v1.0.19–1.0.20) |
| M14 主题系统全面化:`--ot-*` 语义变量层、浅色主题 ANSI 调色板重做到 WCAG AA、MCR/bold/antd 算法明暗自适应、`css-theme-vars` fail-closed 守门 | ✅(2026-10-16,设计/计划见 `docs/superpowers/`;测试 22→25) |
| M14 主题系统全面化:`--ot-*` 语义变量层、浅色主题 ANSI 调色板重做到 WCAG AA、MCR/bold/antd 算法明暗自适应、`css-theme-vars` + `ot-var-names` fail-closed 守门(后者抓住 `--ot-*` 变量名 camelCase/kebab-case 不匹配——9 个多词变量在浅色下静默回落到深色兜底) | ✅(2026-10-16,设计/计划见 `docs/superpowers/`;测试 25→29) |
## 锁屏实现要点(v1.0.17+,已落地)
+1 -1
View File
@@ -13,7 +13,7 @@
"preview": "electron-vite preview",
"typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
"pretest": "npm run typecheck",
"test": "node tests/build-bundles.cjs && node tests/ssh-loopback.mjs && node tests/commands-store.mjs && node tests/connections-store.mjs && node tests/settings-store.mjs && node tests/local-path-grants.mjs && node tests/lock-store.mjs && node tests/lock-controller.mjs && node tests/lock-shortcuts.mjs && node tests/reserved-accelerators.mjs && node tests/.terminal-title.cjs && node tests/ipc-guard.mjs && node tests/updater-fallback.mjs && node tests/log-sanitizer.mjs && node tests/app-log.mjs && node tests/sftp-timeout.mjs && node tests/.hl-split-smoke.cjs && node tests/.hl-rules.cjs && node tests/zmodem-e2e.mjs && node tests/ssh-session-e2e.mjs && node tests/sysinfo-e2e.mjs && node tests/terminal-cwd.mjs && node tests/terminal-links.mjs && node tests/broadcast-store.mjs && node tests/write-batcher.mjs && node tests/refit-throttle.mjs && node tests/semantic-theme.mjs && node tests/theme-contrast.mjs && node tests/css-theme-vars.mjs",
"test": "node tests/build-bundles.cjs && node tests/ssh-loopback.mjs && node tests/commands-store.mjs && node tests/connections-store.mjs && node tests/settings-store.mjs && node tests/local-path-grants.mjs && node tests/lock-store.mjs && node tests/lock-controller.mjs && node tests/lock-shortcuts.mjs && node tests/reserved-accelerators.mjs && node tests/.terminal-title.cjs && node tests/ipc-guard.mjs && node tests/updater-fallback.mjs && node tests/log-sanitizer.mjs && node tests/app-log.mjs && node tests/sftp-timeout.mjs && node tests/.hl-split-smoke.cjs && node tests/.hl-rules.cjs && node tests/zmodem-e2e.mjs && node tests/ssh-session-e2e.mjs && node tests/sysinfo-e2e.mjs && node tests/terminal-cwd.mjs && node tests/terminal-links.mjs && node tests/broadcast-store.mjs && node tests/write-batcher.mjs && node tests/refit-throttle.mjs && node tests/semantic-theme.mjs && node tests/theme-contrast.mjs && node tests/css-theme-vars.mjs && node tests/ot-var-names.mjs",
"predist": "node scripts/verify-deps.cjs && npm test && npm install --package-lock-only",
"dist": "electron-vite build && electron-builder --win nsis",
"dist:dir": "electron-vite build && electron-builder --win --dir"
+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
}
+86
View File
@@ -0,0 +1,86 @@
/**
* Fail-closed guard for the --ot-* semantic layer's NAMES (not values).
*
* CSS custom properties are case-sensitive, so `setProperty('--ot-dangerFg')`
* does NOT satisfy `var(--ot-danger-fg)`: the reference just falls back to the
* dark :root literal and the light theme silently loses its fix. Value tests
* (semantic-theme / theme-contrast) cannot see that, so this test locks the
* three sets together:
* 1) every var(--ot-*) referenced in renderer source is actually emitted;
* 2) the :root static fallback block declares exactly the emitted names;
* 3) both semanticVars() branches emit the same key set (a key that only
* exists on one branch leaves the other theme with a stale :root value).
* Also self-tests the kebab converter so a no-op can't make (1) vacuous.
* Run: node tests/ot-var-names.mjs (must exit 0)
*/
import fs from 'node:fs'
import path from 'node:path'
import { cssVarName, semanticCssVars } from '../src/renderer/src/theme/semantic.ts'
let failed = 0
const ok = (cond, msg) => {
console.log(` ${cond ? 'ok' : 'FAIL'}: ${msg}`)
if (!cond) failed += 1
}
const ROOT = path.resolve('src/renderer/src')
// The naming lives in semantic.ts (cssVarName) and is the SAME function
// chrome.ts calls via semanticCssVars — so a regression there fails here.
const names = (vars) => new Set(Object.keys(vars).filter((k) => k !== 'color-scheme'))
// --- self-test: the converter must actually convert ------------------------
console.log('cssVarName:')
ok(cssVarName('textMuted') === '--ot-text-muted', 'textMuted -> --ot-text-muted')
ok(cssVarName('dangerFg') === '--ot-danger-fg', 'dangerFg -> --ot-danger-fg')
ok(cssVarName('text') === '--ot-text', 'text -> --ot-text (unchanged)')
ok(cssVarName('colorScheme') === 'color-scheme', 'colorScheme -> color-scheme')
// --- what JS emits, per branch ---------------------------------------------
const darkVars = semanticCssVars({ background: '#1e1e1e', foreground: '#cccccc' }, false)
const lightVars = semanticCssVars({ background: '#f6f8fa', foreground: '#383a42' }, true)
const darkNames = names(darkVars)
const lightNames = names(lightVars)
console.log('branches emit the same key set:')
const onlyDark = [...darkNames].filter((n) => !lightNames.has(n)).sort()
const onlyLight = [...lightNames].filter((n) => !darkNames.has(n)).sort()
ok(onlyDark.length === 0, `no dark-only keys${onlyDark.length ? ` (${onlyDark.join(', ')})` : ''}`)
ok(onlyLight.length === 0, `no light-only keys${onlyLight.length ? ` (${onlyLight.join(', ')})` : ''}`)
ok(darkVars['color-scheme'] === 'dark' && lightVars['color-scheme'] === 'light', 'color-scheme switches per branch')
// --- what the CSS references ------------------------------------------------
function walk(dir, out = []) {
for (const f of fs.readdirSync(dir)) {
const p = path.join(dir, f)
if (fs.statSync(p).isDirectory()) walk(p, out)
else if (/\.(css|tsx|ts)$/.test(f)) out.push(p)
}
return out
}
const referenced = new Set()
for (const file of walk(ROOT)) {
for (const m of fs.readFileSync(file, 'utf8').matchAll(/var\((--ot-[A-Za-z0-9-]+)/g)) referenced.add(m[1])
}
console.log('every referenced var is emitted:')
const dangling = [...referenced].filter((n) => !darkNames.has(n)).sort()
ok(dangling.length === 0, dangling.length ? `dangling: ${dangling.join(', ')}` : `${referenced.size} reference(s) all emitted`)
// --- the static :root block in global.css ----------------------------------
const css = fs.readFileSync(path.join(ROOT, 'global.css'), 'utf8')
const rootBlock = /:root\s*\{([\s\S]*?)\}/.exec(css)
const declared = new Set()
if (rootBlock) for (const m of rootBlock[1].matchAll(/(--ot-[A-Za-z0-9-]+)\s*:/g)) declared.add(m[1])
console.log(':root fallback block matches the emitted set (no dark-only leftovers):')
const undeclared = [...darkNames].filter((n) => !declared.has(n)).sort()
const extra = [...declared].filter((n) => !darkNames.has(n)).sort()
ok(rootBlock !== null, ':root block found in global.css')
ok(undeclared.length === 0, undeclared.length ? `emitted but not declared: ${undeclared.join(', ')}` : 'all emitted names declared')
ok(extra.length === 0, extra.length ? `declared but never emitted: ${extra.join(', ')}` : 'no extra declared names')
if (failed > 0) {
console.error(`ot-var-names: ${failed} check(s) failed`)
process.exit(1)
}
console.log('ot-var-names: all ok')
+2 -2
View File
@@ -23,7 +23,7 @@ ok(isLightTheme('#282a36') === false, '#282a36 is dark')
console.log('dark branch locks anchors (zero regression):')
const dark = semanticVars({ background: '#1e1e1e', foreground: '#cccccc' }, false)
ok(dark['muted'] === SEMANTIC.muted, 'muted = anchor')
ok(dark['textMuted'] === SEMANTIC.muted, 'textMuted = anchor')
ok(dark['success'] === SEMANTIC.success, 'success = anchor')
ok(dark['danger'] === SEMANTIC.danger, 'danger = anchor')
ok(dark['warning'] === SEMANTIC.warning, 'warning = anchor')
@@ -44,7 +44,7 @@ for (const bg of ['#f6f8fa', '#fafafa', '#fdf6e3']) {
const x = L(a), y = L(b)
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05)
}
for (const key of ['muted', 'textSecondary', 'successFg', 'dangerFg', 'warningFg', 'infoFg']) {
for (const key of ['textMuted', 'textSecondary', 'successFg', 'dangerFg', 'warningFg', 'infoFg']) {
ok(cr(v[key], bg) >= 4.5, `${bg} ${key} ${v[key]} >= 4.5 (${cr(v[key], bg).toFixed(2)})`)
}
for (const key of ['success', 'danger', 'warning', 'info']) {