feat(settings): replicate the WorkBuddy appearance panel and dark theme (TODO 11)

Recover the WorkBuddy 5.8.3 appearance page from REA evidence (appearance-DMmhwsxq.css + tokens-Df-i-zsO.css): 44 recovered .appearance-* rules, scoped light/dark tokens, original geometry (panel [232,40,638,640], preview 634x317, grid 634x218.391, cards 118.797x79.188) and the two REA-exported preview images with provenance sha256.

Wire the panel into SettingsDialog with only the requested light/dark cards, add scoped tokens to the settings shell so the dark theme no longer renders a white window with near-white text, and verify with scripts/verify-appearance-settings.cjs (21 assertions: geometry, instant switch, cross-window sync, persistence, dark/light surface sampling with contrast >= 4.5, no renderer errors). Update the legacy nav/account/desktop appearance verifiers to the new .appearance-card selectors.
This commit is contained in:
xiaohan.yuan committed 2026-10-17 09:27:59 +08:00
1 parent 7b68d9eb25
commit b36b52831b
25 files changed
+727 -21

No files matched your search

+42 -2
View File
@@ -11,6 +11,9 @@ const windowCss = module.source.artifact.text
if (sha(windowCss) !== '8036657ec27f8948c6bad60c835d62469e958db4a5b9c25e22e5a9e60c0ea986') throw new Error('Window CSS changed')
const contexts = await fs.readFile('.tmp/rea-css-recovered/contexts-BkjwYSZT.css', 'utf8')
if (sha(contexts) !== '50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380') throw new Error('Contexts CSS changed')
const tokens = await fs.readFile('.tmp/rea-css-recovered/tokens-Df-i-zsO.css', 'utf8')
const tokensSha = sha(tokens)
if (tokensSha !== '13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696') throw new Error('Tokens CSS changed')
function project(node) {
if (node.type === 'rule') {
const selectors = node.selectors.filter(selector => /\.settings-(modal|navigation)/.test(selector))
@@ -23,5 +26,42 @@ function project(node) {
return null
}
const root = postcss.root({ nodes: postcss.parse(contexts).nodes.map(project).filter(Boolean) })
await fs.writeFile('src/renderer/src/components/settings-shell-source.css', `/* REA ${evidenceId}; settings-window CSS sha256=${sha(windowCss)}; contexts sha256=${sha(contexts)}. */\n${root.toString()}\n${windowCss}\n`)
console.log('Recovered settings shell CSS with original media conditions')
// 原版把 `--wb-*` 设计变量放在全局 :root(tokens 样式表);YCode 没有全局 token 表,按组件作用域复制。
// 设置窗口的根元素是 `.settings-window-root`(原生设置窗口)或 `.settings-modal-overlay`(主窗口内浮层),
// 而 `.settings-modal` 自身也直接引用这些变量(背景、圆角、滚动条),因此两处都定义一份,
// 深色块让 [data-theme=dark] 下的窗口背景/文字/滚动条随主题切换(TODO 11)。
const lightTokens = new Map()
const darkTokens = new Map()
for (const rule of postcss.parse(tokens).nodes) {
if (rule.type !== 'rule') continue
const selectors = rule.selectors ?? []
const dark = selectors.some(selector => selector.includes('[data-theme=dark]'))
const light = selectors.some(selector => selector.trim() === ':root')
if (!dark && !light) continue
for (const node of rule.nodes ?? []) {
if (node.type !== 'decl' || !node.prop.startsWith('--')) continue
if (dark) darkTokens.set(node.prop, node.value)
if (light) lightTokens.set(node.prop, node.value)
}
}
const referenced = new Set()
const collect = (text) => { for (const match of text.matchAll(/var\(\s*(--[\w-]+)/g)) referenced.add(match[1]) }
collect(root.toString())
collect(windowCss)
for (let depth = 0; depth < 6; depth++) {
const before = referenced.size
for (const name of [...referenced]) {
const value = lightTokens.get(name) ?? darkTokens.get(name)
if (typeof value === 'string') collect(value)
}
if (referenced.size === before) break
}
const missing = [...referenced].filter(name => !lightTokens.has(name))
if (missing.length) console.log(`Tokens missing from light root scope: ${missing.join(', ')}`)
const pick = (map) => new Map([...referenced].filter(name => map.has(name)).map(name => [name, map.get(name)]))
const scope = (selectors, map) => `${selectors}{${[...map].map(([property, value]) => `${property}:${value}`).join(';')}}`
const shellScope = '.settings-window-root,.settings-modal'
const darkScope = ':root[data-theme=dark] .settings-window-root,:root[data-theme=dark] .settings-modal'
const scopedTokens = `${scope(shellScope, pick(lightTokens))}\n${scope(darkScope, pick(darkTokens))}\n`
await fs.writeFile('src/renderer/src/components/settings-shell-source.css', `/* REA ${evidenceId}; settings-window CSS sha256=${sha(windowCss)}; contexts sha256=${sha(contexts)}; tokens sha256=${tokensSha}. */\n${root.toString()}\n${windowCss}\n${scopedTokens}`)
console.log(`Recovered settings shell CSS with original media conditions; scoped ${pick(lightTokens).size} light / ${pick(darkTokens).size} dark tokens`)