Files
ycode/scripts/rea/recover-settings-shell.mjs
xiaohan.yuan b36b52831b 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.
2026-10-17 09:27:59 +08:00

68 lines
4.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Recover only already-retained REA CSS; never access the installed application here.
import fs from 'node:fs/promises'
import { createHash } from 'node:crypto'
import postcss from 'postcss'
const sha = value => createHash('sha256').update(value).digest('hex')
const evidenceId = 'ev_40da6df0424193923bae64a8ccb6fca97ea67813d7de671986d4d7e154b848c1'
const evidence = JSON.parse(await fs.readFile(`.tmp/rea-css-evidence/${evidenceId}.json`, 'utf8'))
if (evidence.evidence_id !== evidenceId) throw new Error('REA identity mismatch')
const module = evidence.normalized_result.styles.items.find(item => /settings-window-perf-ILa933LI.css$/.test(item.file_path))
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))
return selectors.length ? node.clone({ selector: selectors.join(',') }) : null
}
if (node.type === 'atrule' && node.nodes) {
const nodes = node.nodes.map(project).filter(Boolean)
return nodes.length ? node.clone({ nodes }) : null
}
return null
}
const root = postcss.root({ nodes: postcss.parse(contexts).nodes.map(project).filter(Boolean) })
// 原版把 `--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`)