feat(settings): replicate the WorkBuddy general settings panel (TODO 10)

This commit is contained in:
xiaohan.yuan committed 2026-10-17 08:53:34 +08:00
1 parent 401926d7eb
commit 7b68d9eb25
21 files changed
+1150 -11

No files matched your search

@@ -0,0 +1,11 @@
{
"include_fonts": false,
"include_message_content": false,
"track_selectors": [
".settings-modal__panel",
".general-settings-panel"
],
"actions": [
{ "kind": "scroll", "step_id": "return-general-top", "selector": ".settings-modal__panel", "delta_y": -1000, "frames": 1 }
]
}
@@ -0,0 +1,17 @@
{
"include_fonts": false,
"include_message_content": false,
"track_selectors": [
".settings-modal__panel",
".general-settings-panel",
".general-settings-panel__group",
".general-settings-panel__group-card",
".general-settings-panel__section"
],
"actions": [
{ "kind": "wait", "step_id": "general-top", "duration_ms": 250, "frames": 1 },
{ "kind": "scroll", "step_id": "general-scroll-1", "selector": ".settings-modal__panel", "delta_y": 400, "frames": 1 },
{ "kind": "scroll", "step_id": "general-scroll-2-to-bottom", "selector": ".settings-modal__panel", "delta_y": 400, "frames": 1 },
{ "kind": "scroll", "step_id": "return-general-top", "selector": ".settings-modal__panel", "delta_y": -1000, "frames": 1 }
]
}
@@ -0,0 +1,14 @@
{
"include_fonts": false,
"include_message_content": false,
"track_selectors": [
".settings-modal__panel",
".general-settings-panel",
".general-settings-panel__group",
".general-settings-panel__section"
],
"actions": [
{ "kind": "click", "step_id": "select-general-tab", "selector": ".settings-navigation__item", "index": 0, "frames": 0 },
{ "kind": "wait", "step_id": "general-ready", "duration_ms": 500, "frames": 1 }
]
}
@@ -0,0 +1,16 @@
{
"include_fonts": false,
"include_message_content": false,
"track_selectors": [
".user-menu-popover",
".user-menu-items",
".settings-modal__panel",
".general-settings-panel"
],
"actions": [
{ "kind": "click", "step_id": "open-account-menu", "selector": ".user-menu-trigger--workbuddy", "index": 0, "frames": 0 },
{ "kind": "wait", "step_id": "account-menu-ready", "duration_ms": 500, "frames": 0 },
{ "kind": "click", "step_id": "open-settings", "selector": ".user-menu-items .user-menu-item", "index": 3, "frames": 0 },
{ "kind": "wait", "step_id": "settings-ready", "duration_ms": 800, "frames": 1 }
]
}
+111
View File
@@ -0,0 +1,111 @@
// Recover only already-retained REA CSS; never access the installed application here.
//
// Source: REA evidence ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94
// (WorkBuddy 5.8.x settings window), projected through the retained contexts stylesheet
// `.tmp/rea-css-recovered/contexts-BkjwYSZT.css`. The general settings panel DOM classes were
// observed in REA evidence ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e
// (settings window layout: `general-settings-panel`, `font-size-slider`, `wb-storage`,
// `power-save-blocker-settings`, `proxy-section`).
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 sourceEvidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'
const layoutEvidenceId = 'ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e'
const contexts = await fs.readFile('.tmp/rea-css-recovered/contexts-BkjwYSZT.css', 'utf8')
const contextsSha = sha(contexts)
if (contextsSha !== '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')
// UI kit:设置窗口实际加载的控件样式表(按钮 / 输入框 / 开关 / 图标)。
const kit = await fs.readFile('.tmp/rea-css-recovered/src-Bh3YZ35H.css', 'utf8')
const kitSha = sha(kit)
if (kitSha !== '88dbdc1cd3f59d0e8b75824512a465a2be714bc7af481d790fe3a9484f85e01a') throw new Error('UI kit CSS changed')
// 选择器白名单:通用设置面板本体、字号滑块、存储卡片、电源与代理相关规则。
const panelled = /\.(general-settings-panel|font-size-slider|wb-storage|power-save-blocker-settings|proxy-section)(?![a-zA-Z0-9])/
function project(node) {
if (node.type === 'rule') {
const selectors = node.selectors.filter(selector => panelled.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 parsed = postcss.parse(contexts)
const root = postcss.root({ nodes: parsed.nodes.map(project).filter(Boolean) })
// 原版的 .wb-button / .wb-input / .wb-switch / .wb-icon 来自全局 UI kit;YCode 里同样收敛到面板作用域。
// 用 :where() 前缀保持原版层叠:UI kit 规则与面板规则同权重,面板规则在后(原版面板 chunk 后加载),
// 例如 .wb-storage__open-folder{height:auto} 必须仍能覆盖 .wb-button--medium{height:32px}。
const kitScoped = /^\.wb-(button|input|switch|icon)(?![a-zA-Z])/
function projectKit(node) {
if (node.type === 'rule') {
const selectors = node.selectors.filter(selector => kitScoped.test(selector.trim()))
return selectors.length ? node.clone({ selector: selectors.map(selector => `:where(.general-settings-panel) ${selector}`).join(',') }) : null
}
if (node.type === 'atrule' && node.nodes) {
const nodes = node.nodes.map(projectKit).filter(Boolean)
return nodes.length ? node.clone({ nodes }) : null
}
return null
}
const kitRoot = postcss.root({ nodes: postcss.parse(kit).nodes.map(projectKit).filter(Boolean) })
// 原版把 `--wb-*` 设计变量放在全局 :root(tokens 样式表),YCode 的既有做法是按组件作用域
// 复制一份,避免全局污染。这里同样把 tokens 的 :root / 深色作用域合并成通用面板作用域。
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)
}
}
// UI kit 的控件 token 定义在它自己的样式表里。
for (const rule of postcss.parse(kit).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)
}
}
if (lightTokens.size < 200 || darkTokens.size < 200) throw new Error('Tokens CSS lacks root scopes')
// 只输出面板规则真正引用到的变量(含 var() 传递闭包),避免把整套全局 token 复制进面板作用域。
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(kitRoot.toString())
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 = (name, map) => `.${name}{${[...map].map(([property, value]) => `${property}:${value}`).join(';')}}`
const scopedTokens = `${scope('general-settings-panel', pick(lightTokens))}\n:root[data-theme=dark] ${scope('general-settings-panel', pick(darkTokens))}\n`
const css = `/* REA ${sourceEvidenceId} via contexts sha256=${contextsSha}; tokens sha256=${tokensSha}; ui-kit sha256=${kitSha}; general settings panel classes from REA ${layoutEvidenceId}. */\n${kitRoot.toString()}\n${root.toString()}\n${scopedTokens}`
await fs.writeFile('src/renderer/src/components/source-general-settings-recovered.css', css)
const rules = []
root.walkRules(rule => rules.push(rule.selector))
console.log(`Recovered ${rules.length} general-settings rules (${css.length} bytes)`)
console.log(rules.slice(0, 12).join('\n'))
if (rules.length < 60) throw new Error('Unexpectedly few general settings rules')
console.log(`Scoped ${pick(lightTokens).size} light / ${pick(darkTokens).size} dark referenced design tokens into .general-settings-panel`)