feat(settings): replicate the WorkBuddy about and help panels without external jumps (TODO 19)
Recover the shared WorkBuddy 5.8.3 HelpFeedbackPanel stylesheet through REA (18 module rules plus scoped light/dark tokens) and rebuild the 关于 and 获取帮助 pages: 638x620 panels, 638x108 two-row cards, 82x32/102x32 actions, the 638x226 follow card with three 120x120 QR frames and the 638x44 footer. Keep the user's no-jump requirement explicit: every external entry (检查更新, 前往官网, 填写反馈, 查看文档, 隐私政策, 服务协议) keeps the original wording and geometry but is aria-disabled with a 暂未接入/尚未配置 tooltip and performs no navigation; the WorkBuddy account QR codes become same-size 尚未配置 placeholders. The version line reads the real appInfo version. Also fix a CSS bundling regression found while wiring this page: the shared general-settings stylesheet was inlined three times and overrode the personalization long-card padding, collapsing the 598x160 previews to 614 wide. Remove the duplicate import and raise the personalization padding specificity. Verify with scripts/verify-about-help-settings.cjs (15 assertions: both pages, geometry, real version, disabled jumpless actions, no anchors, dark surfaces) and keep all existing settings regressions green.
This commit is contained in:
1 parent
0dc088f1f7
commit
b3be45715e
20 files changed
+683
-7
No files matched your search
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".help-feedback-panel__card",
|
||||
".help-feedback-panel__action-button",
|
||||
".help-feedback-panel__footer"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "check-for-updates-without-installing", "selector": ".help-feedback-panel__action-button", "index": 0, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "observe-update-check-result", "duration_ms": 1200, "frames": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-navigation__item--active",
|
||||
".settings-navigation__group--about",
|
||||
".settings-modal__panel",
|
||||
".help-feedback-panel"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-about-by-observed-nav-index-14-in-dark-theme", "selector": ".settings-navigation__item", "index": 14, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "verify-about-dark-page-stable", "duration_ms": 800, "frames": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"include_fonts": true,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-navigation",
|
||||
".settings-navigation__item--active",
|
||||
".settings-navigation__group--about",
|
||||
".settings-modal__panel"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "scroll", "step_id": "reveal-bottom-about-us-navigation-group", "selector": ".settings-navigation", "delta_y": 500, "frames": 1 },
|
||||
{ "kind": "click", "step_id": "open-about-by-screenshot-confirmed-nav-index-14", "selector": ".settings-navigation__item", "index": 14, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "verify-about-page-stable", "duration_ms": 800, "frames": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "wait", "step_id": "wait-for-theme-renderer-to-stabilize-after-target-change", "duration_ms": 800, "frames": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-navigation__item--active",
|
||||
".settings-navigation__group--about",
|
||||
".settings-modal__panel",
|
||||
".help-feedback-panel"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-get-help-by-observed-nav-index-15-in-dark-theme", "selector": ".settings-navigation__item", "index": 15, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "verify-get-help-dark-page-stable", "duration_ms": 800, "frames": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-navigation",
|
||||
".settings-navigation__item--active",
|
||||
".settings-navigation__group--about",
|
||||
".settings-modal__panel"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "scroll", "step_id": "reveal-bottom-about-us-navigation-group", "selector": ".settings-navigation", "delta_y": 500, "frames": 1 },
|
||||
{ "kind": "click", "step_id": "open-get-help-by-screenshot-confirmed-nav-index-15", "selector": ".settings-navigation__item", "index": 15, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "verify-get-help-page-stable", "duration_ms": 800, "frames": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-navigation",
|
||||
".settings-navigation__item",
|
||||
".settings-navigation__group--about",
|
||||
".settings-modal__panel"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "scroll", "step_id": "reveal-bottom-about-us-navigation-group", "selector": ".settings-navigation", "delta_y": 500, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "verify-about-and-get-help-labels", "duration_ms": 400, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".settings-navigation__item--active",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-appearance-by-observed-nav-index-3-in-dark-theme", "selector": ".settings-navigation__item", "index": 3, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "verify-dark-theme-before-restoring-light", "duration_ms": 500, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".settings-navigation__item--active",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-appearance-by-observed-nav-index-3", "selector": ".settings-navigation__item", "index": 3, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "confirm-light-appearance-before-dark-sample", "duration_ms": 500, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "select-dark-theme-for-about-help-sample", "selector": ".appearance-card--dark", "index": 0, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "restore-light-theme-after-about-help-samples", "selector": ".appearance-card--light", "index": 0, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Recover only retained REA CSS; never access the installed application here.
|
||||
// Sources: REA light/dark evidence for WorkBuddy 5.8.3 Settings -> About / Get Help.
|
||||
// Both captures identify HelpFeedbackPanel-CQNpj-cs.css.
|
||||
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 evidenceIds = {
|
||||
aboutLight: 'ev_7e063ae669e788666080ec25be3247d2c75504d89d076f9963a737cf69ccf25c',
|
||||
helpLight: 'ev_4d3e8c9e277578ef06a16aa4aad5b4766c6a55744c65beb3e8a14033648476c9',
|
||||
aboutDark: 'ev_ed6dce11ff79cf06e3e01c6c1b5ae3e3cc11eac10b37327ce12c06d5c3447053',
|
||||
helpDark: 'ev_ea353952860b6b59699c26fc72d40740b9a3770fe9a3a6e76515686f4363abe6'
|
||||
}
|
||||
const expectedModuleShas = {
|
||||
light: 'fc5a287a1f5a4a76ede74ba971ff12f340ae2193e1dbef9c8898a9b7c3d894e7',
|
||||
dark: '19ec85a3c3fc11a581bbcdf8182e4d78a32dcf5d71307d3da1bbaf92177af765'
|
||||
}
|
||||
const loadModule = async (evidenceId, theme) => {
|
||||
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: ${evidenceId}`)
|
||||
const module = evidence.normalized_result.styles.items.find(item => /HelpFeedbackPanel-CQNpj-cs\.css$/.test(item.file_path))
|
||||
if (!module) throw new Error(`About/help panel module CSS is absent from ${evidenceId}`)
|
||||
const moduleSha = sha(module.source.artifact.text)
|
||||
if (moduleSha !== expectedModuleShas[theme]) throw new Error(`About/help ${theme} module CSS changed in ${evidenceId}`)
|
||||
return { filePath: module.file_path, css: module.source.artifact.text, sha: moduleSha }
|
||||
}
|
||||
|
||||
const aboutLight = await loadModule(evidenceIds.aboutLight, 'light')
|
||||
const helpLight = await loadModule(evidenceIds.helpLight, 'light')
|
||||
const aboutDark = await loadModule(evidenceIds.aboutDark, 'dark')
|
||||
const helpDark = await loadModule(evidenceIds.helpDark, 'dark')
|
||||
if ([helpLight, aboutDark, helpDark].some(item => item.filePath !== aboutLight.filePath)) throw new Error('About/help panel module path changed across captures')
|
||||
|
||||
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')
|
||||
|
||||
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 relatedSelector = /\.help-feedback-panel(?:[\w-]*)?(?![\w-])/i
|
||||
const darkScope = /^\s*(?:\[data-theme=["']?dark["']?\]|body\[data-vscode-theme-name=["']IDE Night["']\]|body\.vscode-dark|\.vscode-dark|body\.dark|\.dark)(?=\s|$)/i
|
||||
function project(node, mode = 'all') {
|
||||
if (node.type === 'rule') {
|
||||
const selectors = []
|
||||
for (const selector of node.selectors) {
|
||||
if (!relatedSelector.test(selector)) continue
|
||||
const isDark = darkScope.test(selector)
|
||||
if (mode === 'shared' && isDark) continue
|
||||
if (mode === 'dark' && !isDark) continue
|
||||
const mapped = isDark ? selector.replace(darkScope, '[data-theme=dark]') : selector
|
||||
if (!selectors.includes(mapped)) selectors.push(mapped)
|
||||
}
|
||||
return selectors.length ? node.clone({ selector: selectors.join(',') }) : null
|
||||
}
|
||||
if (node.type === 'atrule' && node.nodes) {
|
||||
const nodes = node.nodes.map(child => project(child, mode)).filter(Boolean)
|
||||
return nodes.length ? node.clone({ nodes }) : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const sharedRoot = css => postcss.root({ nodes: postcss.parse(css).nodes.map(node => project(node, 'shared')).filter(Boolean) })
|
||||
const lightRoot = sharedRoot(aboutLight.css)
|
||||
const darkSharedRoot = sharedRoot(aboutDark.css)
|
||||
const darkSpecificRoot = postcss.root({ nodes: postcss.parse(aboutDark.css).nodes.map(node => project(node, 'dark')).filter(Boolean) })
|
||||
const normalizeRules = root => {
|
||||
const rules = []
|
||||
root.walkRules(rule => {
|
||||
const selectors = rule.selectors.map(selector => selector.replace(/\s+/g, ' ').trim()).join(',')
|
||||
const declarations = (rule.nodes ?? [])
|
||||
.filter(node => node.type === 'decl')
|
||||
.map(node => `${node.prop}:${node.value.replace(/\b0px\b/g, '0').replace(/\s+/g, ' ').trim()}`)
|
||||
.join(';')
|
||||
rules.push(`${selectors}{${declarations}}`)
|
||||
})
|
||||
return rules.join('\n')
|
||||
}
|
||||
if (normalizeRules(lightRoot) !== normalizeRules(darkSharedRoot)) throw new Error('Light/dark about-help module rules differ beyond serialization')
|
||||
const root = postcss.root({ nodes: [...lightRoot.nodes, ...darkSpecificRoot.nodes] })
|
||||
const referenced = new Set()
|
||||
const collect = text => {
|
||||
for (const match of text.matchAll(/var\(\s*(--[\w-]+)/g)) referenced.add(match[1])
|
||||
}
|
||||
collect(root.toString())
|
||||
for (let depth = 0; depth < 8; depth++) {
|
||||
const before = referenced.size
|
||||
for (const name of [...referenced]) {
|
||||
for (const value of [lightTokens.get(name), darkTokens.get(name)]) {
|
||||
if (typeof value === 'string') collect(value)
|
||||
}
|
||||
}
|
||||
if (referenced.size === before) break
|
||||
}
|
||||
|
||||
const missingLight = [...referenced].filter(name => !lightTokens.has(name))
|
||||
if (missingLight.length) throw new Error(`Tokens missing from light root scope: ${missingLight.join(', ')}`)
|
||||
const pick = map => new Map([...referenced].map(name => [name, map.get(name) ?? lightTokens.get(name)]))
|
||||
const scope = (selector, map) => `${selector}{${[...map].map(([property, value]) => `${property}:${value}`).join(';')}}`
|
||||
const scopedTokens = `${scope('.help-feedback-panel', pick(lightTokens))}\n${scope('[data-theme=dark] .help-feedback-panel', pick(darkTokens))}\n`
|
||||
const evidenceList = Object.entries(evidenceIds).map(([name, evidenceId]) => `${name}=${evidenceId}`).join('; ')
|
||||
const css = `/* REA ${evidenceList}; HelpFeedbackPanel-CQNpj-cs.css light-sha256=${aboutLight.sha} dark-sha256=${aboutDark.sha}; tokens sha256=${tokensSha}. */\n${root.toString()}\n${scopedTokens}`
|
||||
await fs.writeFile('src/renderer/src/components/source-about-help-panels-recovered.css', css)
|
||||
|
||||
const rules = []
|
||||
root.walkRules(rule => rules.push(rule.selector))
|
||||
const darkRules = []
|
||||
darkSpecificRoot.walkRules(rule => darkRules.push(rule.selector))
|
||||
console.log(`Recovered ${rules.length} about/help rules (${Buffer.byteLength(css)} bytes); ${darkRules.length} dark-specific module rules; scoped ${pick(lightTokens).size} light / ${pick(darkTokens).size} dark tokens`)
|
||||
console.log(rules.join('\n'))
|
||||
if (rules.length < 15) throw new Error('Unexpectedly few about/help rules')
|
||||
Reference in new issue
Block a user