From b3be45715e21915839bedf31796e90f8f751ebcb Mon Sep 17 00:00:00 2001 From: "xiaohan.yuan" Date: Sat, 17 Oct 2026 11:20:01 +0800 Subject: [PATCH] feat(settings): replicate the WorkBuddy about and help panels without external jumps (TODO 19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/复刻待办清单-2026-10-17.md | 32 ++- .../rea/probe-todo19-about-check-updates.json | 14 ++ scripts/rea/probe-todo19-about-dark.json | 14 ++ scripts/rea/probe-todo19-about-light.json | 15 ++ .../rea/probe-todo19-appearance-stable.json | 12 + scripts/rea/probe-todo19-help-dark.json | 14 ++ scripts/rea/probe-todo19-help-light.json | 15 ++ scripts/rea/probe-todo19-navigation.json | 14 ++ .../probe-todo19-open-appearance-dark.json | 14 ++ scripts/rea/probe-todo19-open-appearance.json | 14 ++ scripts/rea/probe-todo19-select-dark.json | 12 + scripts/rea/probe-todo19-select-light.json | 12 + scripts/rea/recover-about-help-panels.mjs | 124 ++++++++++ scripts/verify-about-help-settings.cjs | 215 ++++++++++++++++++ scripts/verify-settings-nav.cjs | 4 +- .../src/components/AboutHelpSettingsPanel.tsx | 114 ++++++++++ .../src/components/SettingsDialog.tsx | 4 +- .../source-about-help-panels-recovered.css | 4 + .../components/source-about-help-panels.css | 34 +++ .../source-personalization-panel.css | 9 + 20 files changed, 683 insertions(+), 7 deletions(-) create mode 100644 scripts/rea/probe-todo19-about-check-updates.json create mode 100644 scripts/rea/probe-todo19-about-dark.json create mode 100644 scripts/rea/probe-todo19-about-light.json create mode 100644 scripts/rea/probe-todo19-appearance-stable.json create mode 100644 scripts/rea/probe-todo19-help-dark.json create mode 100644 scripts/rea/probe-todo19-help-light.json create mode 100644 scripts/rea/probe-todo19-navigation.json create mode 100644 scripts/rea/probe-todo19-open-appearance-dark.json create mode 100644 scripts/rea/probe-todo19-open-appearance.json create mode 100644 scripts/rea/probe-todo19-select-dark.json create mode 100644 scripts/rea/probe-todo19-select-light.json create mode 100644 scripts/rea/recover-about-help-panels.mjs create mode 100644 scripts/verify-about-help-settings.cjs create mode 100644 src/renderer/src/components/AboutHelpSettingsPanel.tsx create mode 100644 src/renderer/src/components/source-about-help-panels-recovered.css create mode 100644 src/renderer/src/components/source-about-help-panels.css diff --git a/docs/复刻待办清单-2026-10-17.md b/docs/复刻待办清单-2026-10-17.md index 6838502..bf5902a 100644 --- a/docs/复刻待办清单-2026-10-17.md +++ b/docs/复刻待办清单-2026-10-17.md @@ -494,11 +494,35 @@ ### 19. 关于与获取帮助 -- [ ] 通过 REA 复刻“关于”“获取帮助”的布局、字体、图标、间距和交互外观。 -- [ ] 不复制原版跳转链接;逐项标注 YCode 自有入口或尚未配置的跳转。 -- [ ] 验收:本应用版本等信息真实;没有误跳 WorkBuddy 官方账户、帮助或其他服务地址。 +- [x] 通过 REA 复刻“关于”“获取帮助”的布局、字体、图标、间距和交互外观。 +- [x] 不复制原版跳转链接;逐项标注 YCode 自有入口或尚未配置的跳转。 +- [x] 验收:本应用版本等信息真实;没有误跳 WorkBuddy 官方账户、帮助或其他服务地址。 -状态:待 REA 对照;布局纳入范围,原版链接排除。 +状态:已实现并通过真实窗口回归(15 项断言);布局 1:1,所有外跳类入口明确标注且不跳转。 + +原版证据: +- REA `ev_7e063ae669e788666080ec25be3247d2c75504d89d076f9963a737cf69ccf25c`(关于·浅色 DOM/layout/computed style + 模块 CSS)、`ev_4d3e8c9e277578ef06a16aa4aad5b4766c6a55744c65beb3e8a14033648476c9`(获取帮助·浅色)。 +- REA `ev_ed6dce11ff79cf06e3e01c6c1b5ae3e3cc11eac10b37327ce12c06d5c3447053`(关于·深色)、`ev_ea353952860b6b59699c26fc72d40740b9a3770fe9a3a6e76515686f4363abe6`(获取帮助·深色)。 +- REA `ev_17cc173b4e9da27258d5378924a16fb0a5e0c7b5fd845bec319361fbdff56e30`:最终浅色关于页顶部稳定态。 +- 原版模块 `HelpFeedbackPanel-CQNpj-cs.css`(浅色 sha256 `fc5a287a…`、2,164 字节;深色 sha256 `19ec85a3…`、2,398 字节;两页共用)。观察范围:桌面端 900×680、两页浅色 + 深色抽查;未覆盖:官网/反馈/文档/隐私/协议按钮的跳转目标(全部未点击)、二维码实际内容(未解码)、更新检查的独立软件更新窗口流程(只观察到 WorkBuddy 5.8.3 已是最新版提示)。 +- 几何:两页 right panel 均为 `[232,40,638,620]`;顶部卡片 638×108、两行 606×32(padding 12×16、行距 20);关于 = 检查更新 82×32 + 前往官网 102×32(外跳箭头)+ 关注卡 638×226(标题居中、三列 120×120 二维码框、内图 102×102)+ 页脚 638×44(隐私政策/服务协议 12px 禁用文字);获取帮助 = 意见反馈/填写反馈 82×32 + 帮助文档/查看文档 102×32(外跳箭头),其余留白。 +- 深色:卡片 `#fff → rgb(31,31,31)`、正文 `rgb(0,0,0) → rgb(242,243,245)`、按钮 `rgb(38,38,38)` 底 + `rgb(61,61,61)` 边、二维码框 `rgb(41,41,41)` 底 + `rgb(58,58,58)` 边、页脚文字白 35%。 + +实现: +- `scripts/rea/recover-about-help-panels.mjs` → `source-about-help-panels-recovered.css`:18 条 `HelpFeedbackPanel` 模块规则 + 16 浅色/16 深色作用域 token(3,926 字节、sha256 `DB3E656A…`),连续重跑一致。 +- `AboutHelpSettingsPanel.tsx` + `source-about-help-panels.css`:按原版类名与几何复刻两页;版本号来自 `window.ycode.app.info()` 的真实 `version`;`检查更新`、`前往官网`、`填写反馈`、`查看文档`、`隐私政策`、`服务协议` 保留原版文案与几何,以 `aria-disabled` + title(`暂未接入`/`尚未配置`)拦截点击,不打开任何地址。 +- 原版三张二维码指向 WorkBuddy 官方账号:按用户「不复刻跳转链接」要求替换为同尺寸 120×120 框内的「尚未配置」占位,不生成、不臆造二维码内容;两页均无 `` 与 WorkBuddy 域名。 +- `SettingsDialog.tsx`:`helpFeedback`(关于)与 `getHelp`(获取帮助)加入 supportedTabs 并渲染。 +- 顺带修正样式复用:通用页样式表被多模块 `@import` 导致打包出 3 份并把个性化长卡内边距覆盖为 0;关于页移除重复 import(回到 2 份),并给个性化长卡加更高特异性的 8px 左右内边距规则,长预览恢复 598×160。 + +验证:`scripts/verify-about-help-settings.cjs`(真实隔离 Electron + 原生设置窗口,900×680)15 项断言全部通过:两页导航启用/选中;关于 panel/顶部卡片/两行/两个动作按钮(82×32、102×32)几何;版本号与 `appInfo.version` 一致;关注卡 638×226 + 标题 + 三列 120×120 占位二维码;页脚 638×44 + 两个 12px 禁用链接与禁用色;无 ``;获取帮助卡片/两行/两按钮几何与标注;深色卡片 `rgb(31,31,31)`、正文 `rgb(242,243,245)`、按钮 `rgb(38,38,38)`/边 `rgb(61,61,61)`、二维码框 `rgb(41,41,41)`;恢复浅色;无渲染器报错。报告 `.tmp/about-help-settings-zXlgtu/report.json`。 +回归:`npm run typecheck`、`npm run build`、`npm run test:chat`、`npm run test:conversation`、`npm run test:conversation-ipc`、`node scripts/verify-settings-nav.cjs`、`node scripts/verify-general-settings.cjs`、`node scripts/verify-appearance-settings.cjs`、`node scripts/verify-shortcuts-settings.cjs`、`node scripts/verify-personalization-settings.cjs`、`node scripts/verify-desktop-appearance.cjs`、`node scripts/verify-account-menu.cjs` 全部通过。 + +遗留: +1. 所有外跳类入口按用户要求不复制链接:`检查更新`(YCode 无更新通道)、`前往官网`、`填写反馈`、`查看文档`、`隐私政策`、`服务协议` 均禁用外观 + `aria-disabled` + title,点击无效果。 +2. 三张二维码为「尚未配置」占位,不指向 WorkBuddy 或任何第三方账号;后续若为 YCode 配置官方渠道,再替换真实二维码资源。 +3. 原版关于页点击检查更新会打开独立「软件更新」窗口(观察到 WorkBuddy 5.8.3 已是最新版);YCode 的更新链路未接入,未复刻该窗口。 +4. 原版两页可见范围内没有版权/许可证文案,YCode 也未添加;如需展示以 YCode 实际发布信息为准另行补充。 ## 后续模块 diff --git a/scripts/rea/probe-todo19-about-check-updates.json b/scripts/rea/probe-todo19-about-check-updates.json new file mode 100644 index 0000000..1a8d30f --- /dev/null +++ b/scripts/rea/probe-todo19-about-check-updates.json @@ -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 } + ] +} diff --git a/scripts/rea/probe-todo19-about-dark.json b/scripts/rea/probe-todo19-about-dark.json new file mode 100644 index 0000000..2d76dca --- /dev/null +++ b/scripts/rea/probe-todo19-about-dark.json @@ -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 } + ] +} diff --git a/scripts/rea/probe-todo19-about-light.json b/scripts/rea/probe-todo19-about-light.json new file mode 100644 index 0000000..e6efd27 --- /dev/null +++ b/scripts/rea/probe-todo19-about-light.json @@ -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 } + ] +} diff --git a/scripts/rea/probe-todo19-appearance-stable.json b/scripts/rea/probe-todo19-appearance-stable.json new file mode 100644 index 0000000..2ad58dd --- /dev/null +++ b/scripts/rea/probe-todo19-appearance-stable.json @@ -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 } + ] +} diff --git a/scripts/rea/probe-todo19-help-dark.json b/scripts/rea/probe-todo19-help-dark.json new file mode 100644 index 0000000..7554c3e --- /dev/null +++ b/scripts/rea/probe-todo19-help-dark.json @@ -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 } + ] +} diff --git a/scripts/rea/probe-todo19-help-light.json b/scripts/rea/probe-todo19-help-light.json new file mode 100644 index 0000000..5c45d3b --- /dev/null +++ b/scripts/rea/probe-todo19-help-light.json @@ -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 } + ] +} diff --git a/scripts/rea/probe-todo19-navigation.json b/scripts/rea/probe-todo19-navigation.json new file mode 100644 index 0000000..2ffc62d --- /dev/null +++ b/scripts/rea/probe-todo19-navigation.json @@ -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 } + ] +} diff --git a/scripts/rea/probe-todo19-open-appearance-dark.json b/scripts/rea/probe-todo19-open-appearance-dark.json new file mode 100644 index 0000000..6d3cae1 --- /dev/null +++ b/scripts/rea/probe-todo19-open-appearance-dark.json @@ -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 } + ] +} diff --git a/scripts/rea/probe-todo19-open-appearance.json b/scripts/rea/probe-todo19-open-appearance.json new file mode 100644 index 0000000..b806acc --- /dev/null +++ b/scripts/rea/probe-todo19-open-appearance.json @@ -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 } + ] +} diff --git a/scripts/rea/probe-todo19-select-dark.json b/scripts/rea/probe-todo19-select-dark.json new file mode 100644 index 0000000..c15ec84 --- /dev/null +++ b/scripts/rea/probe-todo19-select-dark.json @@ -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 } + ] +} diff --git a/scripts/rea/probe-todo19-select-light.json b/scripts/rea/probe-todo19-select-light.json new file mode 100644 index 0000000..bbf4adb --- /dev/null +++ b/scripts/rea/probe-todo19-select-light.json @@ -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 } + ] +} diff --git a/scripts/rea/recover-about-help-panels.mjs b/scripts/rea/recover-about-help-panels.mjs new file mode 100644 index 0000000..0136e3c --- /dev/null +++ b/scripts/rea/recover-about-help-panels.mjs @@ -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') diff --git a/scripts/verify-about-help-settings.cjs b/scripts/verify-about-help-settings.cjs new file mode 100644 index 0000000..876badf --- /dev/null +++ b/scripts/verify-about-help-settings.cjs @@ -0,0 +1,215 @@ +// TODO 19 验收:设置 → 关于 / 获取帮助的 1:1 布局 + 真实版本 + 无 WorkBuddy 跳转 + 深浅主题。 +// 真实隔离 YCode(真实 preload/IPC/渲染进程/文件系统)+ 原生设置窗口,视口 900×680(与原版一致)。 +// +// 原版依据(REA ev_7e063a…/ev_4d3e8c…/ev_ed6dce…/ev_ea3539…/ev_17cc17…): +// 两页 panel [232,40,638,620];顶部卡片 638×108(行 606×32,spacing 20,padding 12×16); +// 关于=检查更新 82×32 + 前往官网 102×32 + 关注卡 638×226(三列 120×120 二维码框)+ 页脚 44; +// 获取帮助=意见反馈/填写反馈 82×32 + 帮助文档/查看文档 102×32。 +const fs = require('node:fs/promises') +const path = require('node:path') +const assert = require('node:assert/strict') + +if (!process.versions.electron) { + ;(async () => { + const directory = await fs.mkdtemp(path.resolve('.tmp/about-help-settings-')) + const data = path.join(directory, 'data'), home = path.join(directory, 'home') + await fs.mkdir(data, { recursive: true }) + await fs.mkdir(home, { recursive: true }) + const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data, YCODE_OMP_HOME: home, + YCODE_DISABLE_GPU: '1', YCODE_DISABLE_SYSTEM_INTEGRATION: '1' } + delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL + const child = require('node:child_process').spawn(require('electron'), + ['--no-sandbox', '--disable-gpu-sandbox', __filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) + let output = '' + child.stdout.on('data', bytes => { output += bytes; if (bytes.toString().includes('ABOUT_HELP_SETTINGS_')) process.stdout.write(bytes) }) + child.stderr.on('data', bytes => { output += bytes }) + const timer = setTimeout(() => child.kill(), 300000) + child.once('exit', async code => { + clearTimeout(timer) + await fs.writeFile(path.join(directory, 'process.log'), output) + console.log(JSON.stringify({ directory, exitCode: code })) + if (code) console.error(output.slice(-4000)) + process.exitCode = code ? 1 : 0 + }) + })().catch(error => { console.error(error); process.exitCode = 1 }) +} else { + const { app, BrowserWindow } = require('electron') + const { pathToFileURL } = require('node:url') + const directory = process.env.YCODE_QA_DIRECTORY + app.setPath('home', path.join(directory, 'home')) + const checks = [], snapshots = {}, rendererErrors = [] + let main, settings, phase = 'boot' + const pause = ms => new Promise(resolve => setTimeout(resolve, ms)) + async function read(source, win = main) { + let timer + try { + return await Promise.race([win.webContents.executeJavaScript(source), new Promise((_, reject) => { + timer = setTimeout(() => reject(Error('Renderer timeout: ' + source.slice(0, 120))), 15000) + })]) + } finally { clearTimeout(timer) } + } + async function wait(source, label, win = main) { + for (let index = 0; index < 300; index++) { const value = await read(source, win); if (value) return value; await pause(40) } + throw Error('Timeout: ' + label) + } + async function step(label) { phase = label; await fs.writeFile(path.join(directory, 'phase.txt'), label) } + function check(label, actual, expected) { + const pass = typeof expected === 'function' ? expected(actual) : JSON.stringify(actual) === JSON.stringify(expected) + checks.push({ label, pass, actual, expected: typeof expected === 'function' ? expected.toString() : expected }) + } + const near = (value, target, tolerance = 0.75) => Math.abs(value - target) <= tolerance + function observe(win) { + win.webContents.on('console-message', event => { if (event.level === 'error' || event.level >= 3) rendererErrors.push({ windowId: win.id, message: event.message }) }) + } + async function openSettings(section) { + await read(`window.ycode.app.openSettings(${JSON.stringify(section)})`) + for (let index = 0; index < 200; index++) { + settings = BrowserWindow.getAllWindows().find(win => win !== main) + if (settings && !settings.webContents.isLoading()) break + await pause(25) + } + assert.ok(settings, 'settings window') + await wait('!!document.querySelector(".settings-window-root")', 'settings root', settings) + await pause(400) + return settings + } + + const layoutSource = `(()=>{ + const round = value => Math.round(value * 1000) / 1000 + const rect = node => { if (!node) return null; const r = node.getBoundingClientRect() + return { x: round(r.x), y: round(r.y), width: round(r.width), height: round(r.height) } } + const panel = document.querySelector('.help-feedback-panel') + const buttons = [...document.querySelectorAll('.help-feedback-panel__action-button')] + return { + panel: rect(panel), + section: panel?.dataset.aboutHelpPanel, + card: rect(document.querySelector('.help-feedback-panel__card')), + rows: [...document.querySelectorAll('.help-feedback-panel__card-row')].map(row => ({ + rect: rect(row), + label: row.querySelector('.help-feedback-panel__card-label')?.childNodes[0]?.textContent?.trim(), + value: row.querySelector('.help-feedback-panel__card-value')?.textContent?.trim() + })), + buttons: buttons.map(button => ({ text: button.textContent.trim(), rect: rect(button), disabled: button.disabled, + ariaDisabled: button.getAttribute('aria-disabled'), + title: button.getAttribute('title'), arrow: !!button.querySelector('.wb-button__icon svg') })), + follow: rect(document.querySelector('.help-feedback-panel__follow-card')), + followTitle: (() => { const node = document.querySelector('.help-feedback-panel__follow-title'); return node ? { ...rect(node), text: node.textContent.trim() } : null })(), + qrList: rect(document.querySelector('.help-feedback-panel__qr-list')), + qrItems: [...document.querySelectorAll('.help-feedback-panel__qr-item')].map(item => ({ + label: item.querySelector('.help-feedback-panel__qr-label')?.textContent?.trim(), + rect: rect(item.querySelector('.help-feedback-panel__qr-container')), + placeholder: item.querySelector('.help-feedback-panel__qr--placeholder')?.textContent?.trim() + })), + footer: rect(document.querySelector('.help-feedback-panel__footer')), + footerLinks: [...document.querySelectorAll('.help-feedback-panel__footer-link')].map(link => ({ + text: link.textContent.trim(), rect: rect(link), disabled: link.disabled, color: getComputedStyle(link).color })), + anchors: [...document.querySelectorAll('a')].map(anchor => anchor.getAttribute('href')).filter(Boolean), + cardBackground: panel ? getComputedStyle(document.querySelector('.help-feedback-panel__card')).backgroundColor : null, + labelColor: (() => { const node = document.querySelector('.help-feedback-panel__card-label'); return node ? getComputedStyle(node).color : null })(), + buttonBackground: buttons[0] ? getComputedStyle(buttons[0]).backgroundColor : null, + buttonBorder: buttons[0] ? getComputedStyle(buttons[0]).borderTopColor : null, + qrBackground: (() => { const node = document.querySelector('.help-feedback-panel__qr-container'); return node ? getComputedStyle(node).backgroundColor : null })(), + theme: document.documentElement.dataset.theme ?? 'light' + }})()` + + ;(async () => { + await import(pathToFileURL(path.resolve('out/main/index.mjs')).href); await app.whenReady() + for (let index = 0; index < 200; index++) { + main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode') + if (main && !main.webContents.isLoading()) break + await pause(25) + } + assert.ok(main, 'main window'); observe(main) + await wait('!!window.ycode', 'preload api') + const appInfo = await read(`window.ycode.app.info()`) + snapshots.appInfo = { name: appInfo.name, version: appInfo.version } + + await step('about-settings') + await openSettings('helpFeedback'); observe(settings) + await wait('!!document.querySelector(".help-feedback-panel")', 'about panel', settings) + const nav = await read(`(()=>{const item=document.querySelector('[data-settings-tab="helpFeedback"]') + return { exists: !!item, disabled: item?.getAttribute('aria-disabled'), active: document.querySelector('.settings-navigation__item--active')?.dataset.settingsTab }})()`, settings) + check('关于 nav entry is enabled and active', [nav.exists, nav.disabled !== 'true', nav.active], [true, true, 'helpFeedback']) + + const light = await read(layoutSource, settings) + snapshots.aboutLight = light + check('关于 panel keeps the recovered 638x620 box at 232,40', + [near(light.panel.x, 232), near(light.panel.y, 40), near(light.panel.width, 638), near(light.panel.height, 620)], [true, true, true, true]) + check('关于 top card keeps 638x108 with two 606x32 rows', + [near(light.card.width, 638), near(light.card.height, 108), + near(light.rows[0].rect.y, 52), near(light.rows[0].rect.width, 606), near(light.rows[0].rect.height, 32), + near(light.rows[1].rect.y, 104)], [true, true, true, true, true, true]) + check('关于 rows show the real YCode version and the recovered labels', + [light.rows[0].label, light.rows[0].value, light.rows[1].label], + ['当前版本', `v${appInfo.version}`, '关于 YCode']) + check('关于 action buttons keep 82x32 / 102x32 and stay explicitly disabled', + [light.buttons.map(button => button.text), near(light.buttons[0].rect.x, 772), near(light.buttons[0].rect.width, 82), + near(light.buttons[1].rect.x, 752), near(light.buttons[1].rect.width, 102), + light.buttons.every(button => button.ariaDisabled === 'true' && near(button.rect.height, 32)), + light.buttons[0].title, light.buttons[1].title, light.buttons[1].arrow], + [['检查更新', '前往官网'], true, true, true, true, true, '暂未接入', '尚未配置', true]) + check('关注卡片 keeps 638x226 with the recovered title and three 120x120 placeholder QRs', + [near(light.follow.y, 164), near(light.follow.width, 638), near(light.follow.height, 226), + light.followTitle.text, light.qrList.width, light.qrItems.map(item => item.label), + light.qrItems.every(item => near(item.rect.width, 120) && near(item.rect.height, 120) && item.placeholder === '尚未配置')], + [true, true, true, '关注我们,获取实用技巧与产品最新动态', 408, ['公众号', '视频号', '腾讯频道'], true]) + check('关于 footer keeps the recovered 44px row with two 12px disabled links', + [near(light.footer.y, 616), near(light.footer.width, 638), near(light.footer.height, 44), + light.footerLinks.map(link => link.text), light.footerLinks.every(link => link.disabled && near(link.rect.height, 20)), + light.footerLinks[0].color, near(light.footerLinks[0].rect.x, 489.438, 0.5), near(light.footerLinks[1].rect.x, 564.563, 0.5)], + [true, true, true, ['隐私政策', '服务协议'], true, 'rgba(0, 0, 0, 0.3)', true, true]) + check('关于 page contains no anchor links (no WorkBuddy jumps)', light.anchors, []) + + await step('help-settings') + await openSettings('getHelp') + await wait(`document.querySelector('.help-feedback-panel')?.dataset.aboutHelpPanel==='getHelp'`, 'help panel', settings) + const helpNav = await read(`(()=>{const item=document.querySelector('[data-settings-tab="getHelp"]') + return { disabled: item?.getAttribute('aria-disabled'), active: document.querySelector('.settings-navigation__item--active')?.dataset.settingsTab }})()`, settings) + check('获取帮助 nav entry is enabled and active', [helpNav.disabled !== 'true', helpNav.active], [true, 'getHelp']) + const help = await read(layoutSource, settings) + snapshots.helpLight = help + check('获取帮助 keeps the recovered 638x620 panel and 638x108 card', + [near(help.panel.width, 638), near(help.panel.height, 620), near(help.card.width, 638), near(help.card.height, 108)], [true, true, true, true]) + check('获取帮助 rows and actions keep the recovered labels and geometry', + [help.rows.map(row => row.label), help.buttons.map(button => button.text), + near(help.buttons[0].rect.x, 772), near(help.buttons[0].rect.width, 82), + near(help.buttons[1].rect.x, 752), near(help.buttons[1].rect.width, 102), + help.buttons.every(button => button.ariaDisabled === 'true' && button.title === '尚未配置'), help.buttons[1].arrow], + [['意见反馈', '帮助文档'], ['填写反馈', '查看文档'], true, true, true, true, true, true]) + check('获取帮助 has no QR block, footer or anchor links', + [help.follow, help.footer, help.anchors], [null, null, []]) + + await step('about-help-dark') + await openSettings('appearance') + await wait('!!document.querySelector(".appearance-card")', 'appearance panel', settings) + await read(`document.querySelector('[aria-label="深色"].appearance-card').click()`, settings) + await wait(`document.documentElement.dataset.theme==='dark'`, 'dark theme', settings) + await openSettings('helpFeedback') + await wait(`document.documentElement.dataset.theme==='dark'&&!!document.querySelector('.help-feedback-panel')`, 'dark about panel', settings) + const dark = await read(layoutSource, settings) + snapshots.aboutDark = dark + check('dark theme keeps the recovered card, text, button and QR surfaces', + [dark.cardBackground, dark.labelColor, dark.buttonBackground, dark.buttonBorder, dark.qrBackground], + ['rgb(31, 31, 31)', 'rgb(242, 243, 245)', 'rgb(38, 38, 38)', 'rgb(61, 61, 61)', 'rgb(41, 41, 41)']) + check('dark theme keeps every recovered geometry', + [near(dark.panel.width, 638), near(dark.panel.height, 620), near(dark.card.height, 108), + near(dark.follow.height, 226), dark.qrItems.every(item => near(item.rect.width, 120))], [true, true, true, true, true]) + await openSettings('appearance') + await wait('!!document.querySelector(".appearance-card")', 'appearance panel again', settings) + await read(`document.querySelector('[aria-label="浅色"].appearance-card').click()`, settings) + await wait(`document.documentElement.dataset.theme==='light'`, 'light restored', settings) + + await step('about-help-errors') + check('no renderer error logs', rendererErrors, []) + + const result = checks.every(item => item.pass) ? 'PASS' : 'FAIL' + await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result, phase, checks, snapshots }, null, 2)) + console.log('ABOUT_HELP_SETTINGS_' + result + ' ' + directory) + for (const failure of checks.filter(item => !item.pass)) console.log('ABOUT_HELP_SETTINGS_FAILURE ' + JSON.stringify(failure)) + app.exit(result === 'PASS' ? 0 : 1) + })().catch(async error => { + console.error(error) + try { await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'ERROR', phase, checks, snapshots, error: String(error?.stack ?? error) }, null, 2)) } catch { /* ignore */ } + app.exit(1) + }) +} diff --git a/scripts/verify-settings-nav.cjs b/scripts/verify-settings-nav.cjs index 22c5286..6cad184 100644 --- a/scripts/verify-settings-nav.cjs +++ b/scripts/verify-settings-nav.cjs @@ -125,10 +125,10 @@ if (!process.versions.electron) { // 原版 4 组;about 组按组件约定渲染为分隔线而不是标题。 check('nav keeps the four recovered groups', [nav.groups.length, nav.dividers], [3, 1]) // 通用(settings) 在 TODO 10 接入后同样可用。 - const expectedDisabled = EXPECTED_TABS.filter(tab => !['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization'].includes(tab)) + const expectedDisabled = EXPECTED_TABS.filter(tab => !['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization', 'helpFeedback', 'getHelp'].includes(tab)) check('unwired entries stay disabled', nav.tabs.filter((tab, index) => nav.disabled[index] === 'true'), expectedDisabled) check('unwired entries keep the 暂未接入 hint', nav.titles.filter(title => (title ?? '').includes('暂未接入')).length, expectedDisabled.length) - check('settings, appearance, keyboard shortcuts, personalization and models stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['settings', 'appearance', 'keyboardShortcuts', 'personalization', 'models']) + check('settings, appearance, shortcuts, personalization, models, about and help stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['settings', 'appearance', 'keyboardShortcuts', 'personalization', 'models', 'helpFeedback', 'getHelp']) await step('settings-search-excludes-removed') await read(`(()=>{const input=document.querySelector('.settings-navigation input[type="search"]'); diff --git a/src/renderer/src/components/AboutHelpSettingsPanel.tsx b/src/renderer/src/components/AboutHelpSettingsPanel.tsx new file mode 100644 index 0000000..210829c --- /dev/null +++ b/src/renderer/src/components/AboutHelpSettingsPanel.tsx @@ -0,0 +1,114 @@ +/** + * 设置 → 关于 / 获取帮助:WorkBuddy 5.8.3 两页布局的 1:1 复刻(TODO 19)。 + * + * REA 依据(设置窗口 900×680): + * - ev_7e063ae669e788666080ec25be3247d2c75504d89d076f9963a737cf69ccf25c(关于·浅色 DOM/layout/computed style + 模块 CSS) + * - ev_4d3e8c9e277578ef06a16aa4aad5b4766c6a55744c65beb3e8a14033648476c9(获取帮助·浅色) + * - ev_ed6dce11ff79cf06e3e01c6c1b5ae3e3cc11eac10b37327ce12c06d5c3447053(关于·深色) + * - ev_ea353952860b6b59699c26fc72d40740b9a3770fe9a3a6e76515686f4363abe6(获取帮助·深色) + * - ev_17cc173b4e9da27258d5378924a16fb0a5e0c7b5fd845bec319361fbdff56e30(最终浅色关于页顶部) + * + * 几何:两页右侧 panel 均为 [232,40,638,620];顶部卡片 638×108(两行 606×32,行距 20、padding 12×16); + * 关注卡片 638×226(标题居中 + 三列 120×120 二维码框,内图 102×102); + * 页脚 [232,616,638,44],隐私政策 · 服务协议为 12px 禁用文字按钮。 + * + * 用户限定(TODO 19):只复刻布局,不复刻原版跳转链接。YCode 没有官网/反馈/文档/隐私/协议入口, + * 所有链接类按钮保持原版几何与文案,但禁用并标注「尚未配置」;「检查更新」没有更新通道,标注「暂未接入」。 + * 原版三张二维码指向 WorkBuddy 官方账号,替换为同尺寸「尚未配置」占位块,不臆造二维码内容。 + */ +import { useEffect, useState } from 'react' +import './source-about-help-panels.css' + +export type AboutHelpSection = 'helpFeedback' | 'getHelp' + +const QR_ITEMS: ReadonlyArray<{ key: string; label: string }> = [ + { key: 'official-account', label: '公众号' }, + { key: 'channels', label: '视频号' }, + { key: 'tencent-channel', label: '腾讯频道' } +] + +function ExternalArrow(): React.JSX.Element { + return ( + + ) +} + +function UnavailableAction({ label, title, arrow = false }: { label: string; title: string; arrow?: boolean }): React.JSX.Element { + return ( + + + + ) +} + +export default function AboutHelpSettingsPanel({ section }: { section: AboutHelpSection }): React.JSX.Element { + const [version, setVersion] = useState('—') + useEffect(() => { + let active = true + void window.ycode.app.info() + .then((info) => { if (active) setVersion(info.version) }) + .catch(() => { if (active) setVersion('—') }) + return () => { active = false } + }, []) + + const about = section === 'helpFeedback' + return ( +
+
+ {about ? ( + <> +
+
当前版本v{version}
+ +
+
+
关于 YCode
+ +
+ + ) : ( + <> +
+
意见反馈
+ +
+
+
帮助文档
+ +
+ + )} +
+ + {about ? ( + <> +
+
关注我们,获取实用技巧与产品最新动态
+
+ {QR_ITEMS.map((item) => ( +
+
{item.label}
+
+
尚未配置
+
+
+ ))} +
+
+
+ + + +
+ + ) : null} +
+ ) +} diff --git a/src/renderer/src/components/SettingsDialog.tsx b/src/renderer/src/components/SettingsDialog.tsx index 9079361..56eed6c 100644 --- a/src/renderer/src/components/SettingsDialog.tsx +++ b/src/renderer/src/components/SettingsDialog.tsx @@ -5,6 +5,7 @@ import GeneralSettingsPanel from './GeneralSettingsPanel' import AppearanceSettingsPanel from './AppearanceSettingsPanel' import ShortcutsSettingsPanel from './ShortcutsSettingsPanel' import PersonalizationSettingsPanel from './PersonalizationSettingsPanel' +import AboutHelpSettingsPanel from './AboutHelpSettingsPanel' import SourceSettingsNavigation from './SourceSettingsNavigation' import { useSharedTheme } from '../useSharedTheme' import { SOURCE_WINDOWS_SETTINGS_TABS } from './SourceSettingsNavigation' @@ -55,7 +56,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi return () => window.removeEventListener('keydown', escape) }, [onClose]) const modal =
-
{ setError(''); setSection(id === 'models' ? 'model' : id) }} onUnavailable={() => setError('该设置尚未接入')} />
{error &&

{error}

} @@ -65,6 +66,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi {section === 'appearance' && } {section === 'keyboardShortcuts' && } {section === 'personalization' && } + {(section === 'helpFeedback' || section === 'getHelp') && } {section === 'about' &&

YCode

{settings.appInfo?.version}

}
diff --git a/src/renderer/src/components/source-about-help-panels-recovered.css b/src/renderer/src/components/source-about-help-panels-recovered.css new file mode 100644 index 0000000..8448864 --- /dev/null +++ b/src/renderer/src/components/source-about-help-panels-recovered.css @@ -0,0 +1,4 @@ +/* REA aboutLight=ev_7e063ae669e788666080ec25be3247d2c75504d89d076f9963a737cf69ccf25c; helpLight=ev_4d3e8c9e277578ef06a16aa4aad5b4766c6a55744c65beb3e8a14033648476c9; aboutDark=ev_ed6dce11ff79cf06e3e01c6c1b5ae3e3cc11eac10b37327ce12c06d5c3447053; helpDark=ev_ea353952860b6b59699c26fc72d40740b9a3770fe9a3a6e76515686f4363abe6; HelpFeedbackPanel-CQNpj-cs.css light-sha256=fc5a287a1f5a4a76ede74ba971ff12f340ae2193e1dbef9c8898a9b7c3d894e7 dark-sha256=19ec85a3c3fc11a581bbcdf8182e4d78a32dcf5d71307d3da1bbaf92177af765; tokens sha256=13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696. */ +.help-feedback-panel{flex-direction:column;gap:16px;min-height:100%;display:flex}.help-feedback-panel__card,.help-feedback-panel__follow-card{background:var(--wb-bg-primary,#fff);border-radius:12px}.help-feedback-panel__card{flex-direction:column;gap:20px;padding:12px 16px;display:flex}.help-feedback-panel__card-row{justify-content:space-between;align-items:center;gap:20px;min-height:32px;display:flex}.help-feedback-panel__card-label{min-width:0;color:var(--wb-color-text-primary,#000000e6);align-items:center;gap:4px;font-size:14px;font-weight:400;line-height:22px;display:inline-flex}.help-feedback-panel__card-value{color:var(--wb-color-text-tertiary,#00000080);font-size:12px;line-height:20px}.help-feedback-panel__action-button{flex-shrink:0}.help-feedback-panel__action-wrapper{flex-shrink:0;display:inline-flex}.help-feedback-panel__follow-card{flex-direction:column;align-items:center;gap:16px;padding:20px 16px;display:flex}.help-feedback-panel__follow-title{color:var(--wb-color-text-primary,#000000e6);text-align:center;font-size:14px;font-weight:400;line-height:22px}.help-feedback-panel__qr-list{justify-content:center;gap:24px;display:flex}.help-feedback-panel__qr-item{flex-direction:column;align-items:center;gap:8px;display:flex}.help-feedback-panel__qr-label{color:var(--wb-color-text-tertiary,#00000080);font-size:12px;line-height:20px}.help-feedback-panel__qr-container{box-sizing:border-box;background:var(--wb-color-bg-secondary-default,#fff);border:.5px solid var(--wb-color-border-secondary,#ebebeb);border-radius:16px;width:120px;height:120px;padding:8px}.help-feedback-panel__qr{object-fit:contain;background:var(--wb-palette-white-100,#fff);width:100%;height:100%;display:block}.help-feedback-panel__footer{justify-content:center;align-items:center;gap:var(--wb-spacing-4);padding-top:var(--wb-spacing-7);margin-top:auto;display:flex}.help-feedback-panel__footer-link.wb-button{min-width:0;height:auto;color:var(--wb-color-text-disabled,#0000004d);box-shadow:none;padding:0;font-size:12px;line-height:20px}.help-feedback-panel__footer-link.wb-button:hover:not(:disabled){color:var(--wb-color-text-tertiary,#00000080);background:0 0} +.help-feedback-panel{--wb-bg-primary:var(--wb-palette-white-100);--wb-color-text-primary:var(--wb-palette-black-100);--wb-color-text-tertiary:var(--wb-palette-black-50);--wb-color-bg-secondary-default:var(--wb-palette-white-100);--wb-color-border-secondary:var(--wb-palette-gray-4);--wb-palette-white-100:#fff;--wb-spacing-4:12px;--wb-spacing-7:24px;--wb-color-text-disabled:var(--wb-palette-black-30);--wb-palette-gray-3:#f2f2f2;--wb-palette-black-100:#000;--wb-palette-black-50:#00000080;--wb-palette-gray-2:#f7f7f7;--wb-palette-gray-4:#ebebeb;--wb-palette-gray-5:#e6e6e6;--wb-palette-black-30:#0000004d} +[data-theme=dark] .help-feedback-panel{--wb-bg-primary:var(--wb-palette-gray-3);--wb-color-text-primary:#fff;--wb-color-text-tertiary:#ffffff80;--wb-color-bg-secondary-default:var(--wb-palette-gray-2);--wb-color-border-secondary:var(--wb-palette-gray-5);--wb-palette-white-100:#fff;--wb-spacing-4:12px;--wb-spacing-7:24px;--wb-color-text-disabled:#ffffff59;--wb-palette-gray-3:#1f1f1f;--wb-palette-black-100:#000;--wb-palette-black-50:#00000080;--wb-palette-gray-2:#292929;--wb-palette-gray-4:#242424;--wb-palette-gray-5:#3a3a3a;--wb-palette-black-30:#0000004d} diff --git a/src/renderer/src/components/source-about-help-panels.css b/src/renderer/src/components/source-about-help-panels.css new file mode 100644 index 0000000..cf1f6ea --- /dev/null +++ b/src/renderer/src/components/source-about-help-panels.css @@ -0,0 +1,34 @@ +/* + * 设置 → 关于 / 获取帮助(TODO 19)。 + * - source-about-help-panels-recovered.css:REA 回收的 HelpFeedbackPanel-CQNpj-cs.css 投影。 + * - 按钮所需的 .wb-button UI kit 由 GeneralSettingsPanel 已加载的 source-general-settings.css 提供; + * 这里不再重复 @import,避免同一份通用样式表被打包多次、排到本页规则之后改变层叠。 + * 以下是二维码占位与深色正文的本地适配(原版二维码指向 WorkBuddy 官方账号,按用户要求不复刻)。 + */ +@import './source-about-help-panels-recovered.css'; + +/* + * REA ev_ed6dce11…(关于·深色):正文 color 为 rgb(242,243,245)。 + * 原版设置窗口把 --wb-color-text-primary 覆盖为 #f2f3f5(比全局 tokens 的 #fff 更具体),这里同优先级还原。 + */ +:root[data-theme=dark] .general-settings-panel.help-feedback-panel { + --wb-color-text-primary: #f2f3f5; +} + +/* 原版二维码图片为白底;YCode 无对应账号,用同尺寸占位块并明确标注。 */ +.help-feedback-panel .help-feedback-panel__qr--placeholder { + display: flex; + align-items: center; + justify-content: center; + color: var(--wb-color-text-tertiary); + font-size: 12px; + line-height: 20px; + border: 1px dashed var(--wb-color-border-secondary); + border-radius: 8px; +} + +.help-feedback-panel .help-feedback-panel__footer-separator { + color: var(--wb-color-text-disabled); + font-size: 12px; + line-height: 20px; +} diff --git a/src/renderer/src/components/source-personalization-panel.css b/src/renderer/src/components/source-personalization-panel.css index c669d5e..3d9507d 100644 --- a/src/renderer/src/components/source-personalization-panel.css +++ b/src/renderer/src/components/source-personalization-panel.css @@ -165,3 +165,12 @@ /* 未接入行为保持原版形态,但禁用交互(与通用页约定一致)。 */ .personalization-panel [data-settings-unavailable=true] { cursor: default } .personalization-panel [data-settings-unavailable=true] .wb-switch { cursor: default } + +/* + * 原版长卡左右各 8px 内边距(预览 598 = 614 − 16)。通用页样式表会被多个面板 @import, + * 打包顺序可能排到本页规则之后;这里提高特异性,保证 598×160 的几何不被通用 section 覆盖。 + */ +.general-settings-panel.personalization-panel .local-memory-long .general-settings-panel__section--plain { + padding-left: 8px; + padding-right: 8px; +}