feat(sidebar): replicate WorkBuddy footer account row (TODO 07)

This commit is contained in:
xiaohan.yuan committed 2026-10-17 07:15:38 +08:00
1 parent b212a3e867
commit 0b549ca794
5 files changed
+412 -101

No files matched your search

+30 -4
View File
@@ -186,11 +186,37 @@
### 07. 侧栏最下一行 ### 07. 侧栏最下一行
- [ ] 移除最下方不应存在的“收起菜单”入口。 - [x] 移除最下方不应存在的“收起菜单”入口。
- [ ] 对齐最下一行的布局、高度、内边距、字体、头像、当前图标与悬停状态。 - [x] 对齐最下一行的布局、高度、内边距、字体、头像、当前图标与悬停状态。
- [ ] 验收:普通/窄侧栏、浅色/深色下均无残留入口或空白占位。 - [x] 验收:普通/窄侧栏、浅色/深色下均无残留入口或空白占位。
状态:待 REA 对照。 状态:已对齐并通过真实窗口回归(运行时几何 + 计算值 + hover 目标色);收起入口只在顶栏保留一处。
原版证据:
- 运行时布局 REA Evidence `ev_721d830c97114930e046ec8931bd2f473a11ab23efcab9c727c4256889e29879`:`.conversation-list` 宽 284.28(含 border-right 1px);`.conversation-list-footer` `[0,845,283.28,57]`、padding 8/12/8/8、border-top 1px `rgb(230,230,230)`、display flex + align-items center + justify-content flex-start;`.user-menu-trigger-wrapper` `[8,854,263.28,40]`、gap 4;`.user-menu-trigger--workbuddy` `[8,854,191.28,40]`、min-height 40 / padding 6px 8px / radius 8;`.user-menu-trigger-left` gap 10;头像 28×28 圆形;姓名 12px/600/12px;`.msg-center-bell-btn` 与 `.user-menu-trigger-miniprogram` 各 32×32 / radius 6。
- 回收样式表 `contexts-BkjwYSZT.css`(`.conversation-list-footer{...}`、`.conversation-list-footer .user-menu{width:100%}`)与 `UserMenu-CpHz-0rF.css`(`.user-menu-trigger*`、`.user-menu-trigger--workbuddy:hover{background-color:var(--wb-bg-hover)}`、`.msg-center-bell-btn:hover{background-color:var(--wb-button-ghost-bg-hover);color:var(--wb-icon-primary)}`、`.msg-center-badge{...}`、`.user-menu-avatar-initial{background-color:#0056d4}`)。
- 同一证据中侧栏收起源位于顶栏 `.conversation-list-topbar-actions`(`[8,3,76,24]`,3 个 24×24,x=8/34/60);底部该行只有账号 / 通知 / 小程序三个入口。
- 观察范围:桌面端浅色主题、zoom=1、1336 宽窗口。未覆盖:深色主题运行时像素、原版真实头像图片。
实现:
- `components/Sidebar.tsx`:底部账号区(`.sidebar__footer.conversation-list-footer`)只保留账号触发器、通知铃铛、小程序三个入口;删除底部“收起侧栏”按钮及随之无引用的 `IconCollapseRail`。
- `styles/sidebar.css`:逐条落地回收规则(footer padding/边框/flex、wrapper gap/宽度、触发器 min-height/padding/radius/`background-color .2s`、左组 gap 10、头像 28×28 圆形 + `#0056d4` 字面头像、姓名 12/600/12 省略号、铃铛与小程序 32×32/radius 6/`background-color, color .15s`、角标 14×14/radius 7/top 2/right 2/translate(40%,-30%));hover 直接使用回收 token `--wb-bg-hover`、`--wb-button-ghost-bg-hover`、`--wb-icon-primary`。
- `workbuddy/workbench/source-panel-layout.ts`:展开态侧栏宽度 280 → 284,使整列与 footer 边界对齐原版 284.28(实测 footer `[0,845,283,57]`、wrapper `[8,854,263,40]`、触发器 191×40)。
- 顶栏既有“收起侧边栏”入口(24×24)保持不变;折叠后由 `.sidebar__rail` 的“展开侧栏”恢复。
验证:新增 `scripts/verify-sidebar-footer.cjs`(真实隔离 Electron + 真实 preload/IPC/渲染进程)29 项断言全 PASS:
- 布局:footer padding 8/12/8/8、flex 契约、border-top 1px `rgb(230,230,230)`、高 57;wrapper gap 4 且子元素恰为账号 / 通知 / 小程序;footer 内不存在任何“收起”入口。
- 组件:账号触发器 min-height 40 / padding 6px 8px / radius 8 / 过渡 `background-color .2s`、宽 191;左组 gap 10;头像 28×28 圆形 `rgb(0,86,212)`;姓名 12/600/12 + 省略号;铃铛 32×32 / radius 6 / `rgba(0,0,0,.7)` / `.15s`;小程序 32×32 / radius 6;角标几何与配色(`rgb(246,64,65)`、top/right/translate)。
- hover:先留档“过渡冻结在起始值”现象(`playState: running`、`currentTime: 0`,QA 窗口无合成帧),随后关闭过渡读取目标色——铃铛 hover 背景等于 `--wb-button-ghost-bg-hover` 计算值 `rgba(0,0,0,0.06)`、图标色等于 `--wb-icon-primary` `rgba(0,0,0,0.9)`;账号触发器 hover 背景等于 `--wb-bg-hover` 计算值 `color(srgb 0 0 0 / 0.05)`。
- 收起入口:顶栏 24×24 按钮仍可折叠侧栏,折叠轨道保留“展开侧栏”。
- 回归:侧栏宽度变更后 `node scripts/verify-conversation-scrollbar.cjs`、`node scripts/verify-user-message-edit.cjs` 仍全 PASS;`npm run typecheck`、`npm run build` 通过。
- 报告与截图:`.tmp/sidebar-footer-3edCU8/report.json`、`.tmp/sidebar-footer-3edCU8/sidebar-footer.png`。
遗留:
1. 侧栏宽度取整数 284,与原版运行时 284.28 存在 0.28px 分形残差(四舍五入后可见差异为 0)。
2. 原版头像为真实图片(`.user-menu-trigger-avatar-img:after` 还有依赖 `--wb-avatar-fallback-*` 的 1px 内描边);YCode 无账号体系,用 `#0056d4` 字面初始头像且未接内描边。
3. 深色主题底部行未采集运行时像素,仅按回收 token(white-6% / white-10%)对齐。
4. 通知角标数据与小程序跳转为 mock(属 TODO 09 与待拆解项),本项只对齐入口与样式。
### 08. 新建任务发送后的立即切页与占位 ### 08. 新建任务发送后的立即切页与占位
+205
View File
@@ -0,0 +1,205 @@
// 真实隔离 YCode App + 真实 preload/IPC/渲染进程,验证侧栏底部账号区(TODO 07)。
// 原版期望值:
// - 样式表 contexts-BkjwYSZT.css:.conversation-list-footer{border-top:.5px solid var(--wb-color-border-primary);flex-shrink:0;
// justify-content:flex-start;align-items:center;padding:8px 12px 8px 8px;display:flex} / .conversation-list-footer .user-menu{width:100%}
// - 样式表 UserMenu-CpHz-0rF.css:.user-menu-trigger-wrapper{gap:4px;width:100%;display:flex;position:relative}、
// .user-menu-trigger--workbuddy{box-sizing:border-box;border-radius:8px;flex:1;min-width:0;min-height:40px;padding:6px 8px;overflow:hidden}、
// .user-menu-trigger-left{gap:10px}、头像 28×28 圆形、.user-menu-trigger-name{font-size:12px;font-weight:600;line-height:12px}、
// .msg-center-bell-btn / .user-menu-trigger-miniprogram{32×32;border-radius:6px;transition:background-color .15s,color .15s}、
// .msg-center-badge{min-width:14px;height:14px;border-radius:7px;font-size:9px;line-height:14px;top:2px;right:2px;transform:translate(40%,-30%)}
// - 运行时布局证据 ev_721d830c97114930e046ec8931bd2f473a11ab23efcab9c727c4256889e29879:
// .conversation-list-footer [0,845,283.28,57]、padding 8/12/8/8、border-top 1px rgb(230,230,230);
// .user-menu-trigger-wrapper [8,854,263.28,40];触发器 191.28×40;铃铛/小程序各 32×32;头像 28×28;姓名 12/600/12;
// 同一证据里顶部 .conversation-list-topbar-actions 才承载收起/筛选入口,底部没有收起按钮。
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/sidebar-footer-'))
const data = path.join(directory, 'data')
const home = path.join(directory, 'home')
const cwd = path.join(directory, 'fixture-workspace')
await fs.mkdir(data); await fs.mkdir(home); await fs.mkdir(cwd)
const now = Date.now()
const sessions = [{ id: 'footer-live', title: 'Footer Live', cwd, createdAt: now, updatedAt: now + 1, messages: [
{ id: 'footer-user', role: 'user', content: '底部账号行验收', createdAt: now, status: 'done', requestId: 'footer-run' },
{ id: 'footer-assistant', role: 'assistant', content: '收到。', createdAt: now + 1, status: 'done', requestId: 'footer-run', tools: [], parts: [] }
] }]
await fs.writeFile(path.join(data, 'sessions.json'), JSON.stringify({ sessions }))
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data,
YCODE_OMP_HOME: home, YCODE_DISABLE_GPU: '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('SIDEBAR_FOOTER_')) process.stdout.write(bytes) })
child.stderr.on('data', bytes => { output += bytes })
const timer = setTimeout(() => child.kill(), 180000)
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)
process.exitCode = code ? 1 : 0
})
})()
} else {
const { app, BrowserWindow } = require('electron')
app.disableHardwareAcceleration()
const directory = process.env.YCODE_QA_DIRECTORY
assert.ok(path.relative(path.resolve('.tmp'), directory).startsWith('sidebar-footer-'))
app.setPath('userData', path.join(directory, 'data')); app.setPath('sessionData', path.join(directory, 'data'))
app.setPath('home', path.join(directory, 'home'))
let main, phase = 'boot'
const checks = [], snapshots = {}, rendererErrors = []
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
async function read(source) {
let timer
try {
return await Promise.race([main.webContents.executeJavaScript(source), new Promise((_, reject) => {
timer = setTimeout(() => reject(Error('Renderer timeout: ' + source.slice(0, 100))), 15000)
})])
} catch (error) { throw Error(error.message + ' - ' + source.slice(0, 240)) } finally { clearTimeout(timer) }
}
async function wait(source, label) {
for (let index = 0; index < 250; index++) { const value = await read(source); if (value) return value; await pause(30) }
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 footerSource = `(()=>{const footer=document.querySelector('.conversation-list-footer');if(!footer)return null;
const style=getComputedStyle(footer);const box=footer.getBoundingClientRect();
const wrapper=footer.querySelector('.user-menu-trigger-wrapper');const wstyle=getComputedStyle(wrapper);const wbox=wrapper.getBoundingClientRect();
const trigger=footer.querySelector('.user-menu-trigger--workbuddy');const tstyle=getComputedStyle(trigger);const tbox=trigger.getBoundingClientRect();
const left=footer.querySelector('.user-menu-trigger-left');const avatar=footer.querySelector('.user-menu-trigger-avatar-img');
const name=footer.querySelector('.user-menu-trigger-name');const nstyle=getComputedStyle(name);
const bell=footer.querySelector('.msg-center-bell-btn');const bstyle=getComputedStyle(bell);const bbox=bell.getBoundingClientRect();
const mini=footer.querySelector('.user-menu-trigger-miniprogram');const mstyle=getComputedStyle(mini);const mbox=mini.getBoundingClientRect();
const badge=footer.querySelector('.msg-center-badge');const gstyle=badge?getComputedStyle(badge):null;const gbox=badge?badge.getBoundingClientRect():null;
const abox=avatar.getBoundingClientRect();const abstyle=getComputedStyle(avatar);
const box4=r=>[r.x,r.y,r.width,r.height];
return {footer:{box:box4(box),padding:[style.paddingTop,style.paddingRight,style.paddingBottom,style.paddingLeft],borderTop:style.borderTopWidth,borderColor:style.borderTopColor,display:style.display,alignItems:style.alignItems,justifyContent:style.justifyContent},
wrapper:{box:box4(wbox),gap:wstyle.gap,width:wstyle.width,display:wstyle.display,position:wstyle.position,
children:[...wrapper.children].map(node=>node.className)},
trigger:{box:box4(tbox),minHeight:tstyle.minHeight,padding:tstyle.padding,borderRadius:tstyle.borderRadius,transition:tstyle.transitionProperty+' '+tstyle.transitionDuration,flexGrow:tstyle.flexGrow,flexShrink:tstyle.flexShrink},
left:{gap:left?getComputedStyle(left).gap:null,display:left?getComputedStyle(left).display:null},
avatar:{box:box4(abox),radius:abstyle.borderRadius,background:abstyle.backgroundColor,font:abstyle.fontSize+'/'+abstyle.lineHeight,color:abstyle.color},
name:{font:nstyle.fontSize+'/'+nstyle.lineHeight,weight:nstyle.fontWeight,color:nstyle.color,overflow:nstyle.overflow,textOverflow:nstyle.textOverflow,whiteSpace:nstyle.whiteSpace},
bell:{box:box4(bbox),radius:bstyle.borderRadius,color:bstyle.color,transition:bstyle.transitionProperty+' '+bstyle.transitionDuration},
mini:{box:box4(mbox),radius:mstyle.borderRadius,color:mstyle.color},
badge:badge?{box:box4(gbox),minWidth:gstyle.minWidth,height:gstyle.height,radius:gstyle.borderRadius,font:gstyle.fontSize+'/'+gstyle.lineHeight,weight:gstyle.fontWeight,background:gstyle.backgroundColor,color:gstyle.color,top:gstyle.top,right:gstyle.right,transform:gstyle.transform}:null,
collapseInFooter:[...footer.querySelectorAll('[title]')].map(node=>node.getAttribute('title')).filter(title=>/收起/.test(title))}})()`
;(async () => {
await import('node:url').then(({ pathToFileURL }) => import(pathToFileURL(path.resolve('out/main/index.mjs')).href))
await app.whenReady()
main = BrowserWindow.getAllWindows()[0]
main.setSize(1336, 902)
main.showInactive()
main.webContents.debugger.attach('1.3')
main.webContents.on('console-message', (_event, level, message) => { if (level >= 2) rendererErrors.push(message) })
await wait('!!document.querySelector("#root")', 'renderer root')
await pause(1500)
await step('footer-layout')
await read(`(()=>{const rows=[...document.querySelectorAll('[data-session-id]')];const row=rows.find(item=>item.textContent.includes('Footer Live'));if(!row)return false;row.click();return true})()`)
await wait(`!!document.querySelector('.conversation-list-footer')`, 'sidebar footer')
await pause(400)
const footer = await read(footerSource)
snapshots.footer = footer
check('footer keeps the recovered padding contract', footer.footer.padding.join(','), '8px,12px,8px,8px')
check('footer keeps the recovered flex contract', footer.footer.display === 'flex' && footer.footer.alignItems === 'center' && footer.footer.justifyContent === 'flex-start', true)
check('footer keeps the recovered top border', footer.footer.borderTop === '1px' && footer.footer.borderColor === 'rgb(230, 230, 230)', true)
check('footer height stays 57px like the original', Math.round(footer.footer.box[3]), 57)
check('wrapper matches the recovered gap and width', footer.wrapper.gap === '4px' && footer.wrapper.display === 'flex' && footer.wrapper.position === 'relative', true)
check('wrapper holds exactly account + bell + miniprogram', footer.wrapper.children.join('|'),
'user-menu-trigger user-menu-trigger--workbuddy|msg-center-bell-btn user-menu-trigger-msg-center|user-menu-trigger-miniprogram')
check('footer has no collapse entry any more', footer.collapseInFooter, [])
check('account trigger matches min-height, padding and radius', footer.trigger.minHeight === '40px' && footer.trigger.padding === '6px 8px' && footer.trigger.borderRadius === '8px', true)
check('account trigger keeps the recovered background transition', footer.trigger.transition, 'background-color 0.2s')
check('account trigger squeezes to the recovered width', Math.round(footer.trigger.box[2]), 191)
check('trigger left group keeps gap 10', footer.left.gap, '10px')
check('avatar is a 28x28 circle with the recovered initial colour', Math.round(footer.avatar.box[2]) === 28 && Math.round(footer.avatar.box[3]) === 28 && footer.avatar.radius === '50%' && footer.avatar.background === 'rgb(0, 86, 212)', true)
check('name follows 12/600/12 with ellipsis', footer.name.font === '12px/12px' && footer.name.weight === '600' && footer.name.overflow === 'hidden' && footer.name.textOverflow === 'ellipsis' && footer.name.whiteSpace === 'nowrap', true)
check('bell is 32x32 with radius 6 and the secondary colour', Math.round(footer.bell.box[2]) === 32 && Math.round(footer.bell.box[3]) === 32 && footer.bell.radius === '6px' && footer.bell.color === 'rgba(0, 0, 0, 0.7)', true)
check('bell keeps the recovered hover transition', footer.bell.transition, 'background-color, color 0.15s, 0.15s')
check('miniprogram button matches 32x32 radius 6', Math.round(footer.mini.box[2]) === 32 && Math.round(footer.mini.box[3]) === 32 && footer.mini.radius === '6px', true)
check('unread badge keeps the recovered geometry', footer.badge.minWidth === '14px' && footer.badge.height === '14px' && footer.badge.radius === '7px' && footer.badge.font === '9px/14px', true)
check('unread badge keeps the recovered colours', footer.badge.background === 'rgb(246, 64, 65)' && footer.badge.color === 'rgb(255, 255, 255)', true)
check('unread badge keeps top 2 / right 2 with translate(40%,-30%)', footer.badge.top === '2px' && footer.badge.right === '2px' && /matrix\(1, 0, 0, 1, 5\.6, -4\.2\)/.test(footer.badge.transform), true)
await step('footer-hover')
main.focus(); main.webContents.focus()
await pause(150)
// 用 CDP Input 域驱动真实指针,确保 CSS :hover 生效(sendInputEvent 在无焦点窗口下不可靠)。
const hover = async (selector) => {
const center = await read(`(()=>{const box=document.querySelector(${JSON.stringify(selector)}).getBoundingClientRect();return {x:Math.round(box.x+box.width/2),y:Math.round(box.y+box.height/2)}})()`)
await main.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { type: 'mouseMoved', x: center.x, y: center.y, button: 'none' })
await pause(250)
}
await hover('.msg-center-bell-btn')
await pause(300)
// 无合成帧环境下 transition 停在起始值(透明),先把「冻结」状态留档,再关掉过渡读取目标色。
snapshots.bellHoverFrozen = await read(`(()=>{const bell=document.querySelector('.msg-center-bell-btn');const style=getComputedStyle(bell);
return {hover:bell.matches(':hover'),background:style.backgroundColor,inline:bell.style.backgroundColor||null,
animations:[...document.getAnimations()].filter(animation=>animation.effect&&animation.effect.target===bell).map(animation=>({property:animation.transitionProperty||null,playState:animation.playState,currentTime:animation.currentTime}))}})()`)
snapshots.hoverDiag = await read(`(()=>{const bell=document.querySelector('.msg-center-bell-btn');const trigger=document.querySelector('.user-menu-trigger--workbuddy');const bs=getComputedStyle(bell);const ts=getComputedStyle(trigger);
return {bell:bell.matches(':hover'),trigger:trigger.matches(':hover'),focus:document.hasFocus(),active:document.activeElement?document.activeElement.tagName:null,
bellVars:{ghost:bs.getPropertyValue('--wb-button-ghost-bg-hover'),ycHover:bs.getPropertyValue('--yc-bg-hover'),wbHover:bs.getPropertyValue('--wb-bg-hover')},
triggerVars:{wbHover:ts.getPropertyValue('--wb-bg-hover'),palette:ts.getPropertyValue('--wb-palette-black-100'),ycHover:ts.getPropertyValue('--yc-bg-hover'),bg:ts.backgroundColor}}})()`)
snapshots.varProbe = await read(`(()=>{const bell=document.querySelector('.msg-center-bell-btn');const aside=bell.closest('aside.sidebar');
const chain=[];for(let node=bell;node;node=node.parentElement){chain.push(node.tagName.toLowerCase()+(node.className?'.'+String(node.className).split(/\\s+/).join('.'):''))}
return {chain,asideFound:!!aside,
asideBgHover:aside?getComputedStyle(aside).getPropertyValue('--wb-bg-hover'):null,
bellGhostHover:getComputedStyle(bell).getPropertyValue('--wb-button-ghost-bg-hover'),
bellIconPrimary:getComputedStyle(bell).getPropertyValue('--wb-icon-primary')}})()`)
snapshots.bellRules = await read(`(()=>{const out=[];for(const sheet of document.styleSheets){let rules;try{rules=sheet.cssRules}catch{continue}for(const rule of rules){if(!rule.selectorText||!/msg-center-bell-btn/.test(rule.selectorText))continue;out.push({selector:rule.selectorText,background:rule.style.background,backgroundColor:rule.style.backgroundColor,important:rule.style.getPropertyPriority('background-color'),href:(sheet.href||'inline').split('/').pop()})}}return out})()`)
await read(`(()=>{const style=document.createElement('style');style.id='qa-no-transitions';style.textContent='*{transition:none!important}';document.head.appendChild(style);return true})()`)
await pause(120)
snapshots.hoverTokens = await read(`(()=>{const host=document.querySelector('.conversation-list-footer');
const probe=(property,token)=>{const el=document.createElement('div');el.style[property]='var('+token+')';host.appendChild(el);const value=getComputedStyle(el)[property];el.remove();return value};
return {bgHover:probe('backgroundColor','--wb-bg-hover'),ghostHover:probe('backgroundColor','--wb-button-ghost-bg-hover'),iconPrimary:probe('color','--wb-icon-primary')}})()`)
check('recovered --wb-bg-hover resolves to the 5% black fill', snapshots.hoverTokens.bgHover,
value => /^(color\(srgb 0 0 0 \/ 0\.05\)|rgba\(0, 0, 0, 0\.05\))$/.test(String(value)))
check('recovered --wb-button-ghost-bg-hover resolves to the ~6% black fill', snapshots.hoverTokens.ghostHover,
value => /^rgba\(0, 0, 0, 0\.0(58|59|6)\)$/.test(String(value)))
await hover('.msg-center-bell-btn')
snapshots.bellHover = await read(`(()=>{const bell=document.querySelector('.msg-center-bell-btn');const style=getComputedStyle(bell);return {hover:bell.matches(':hover'),background:style.backgroundColor,color:style.color}})()`)
check('bell hover uses --wb-button-ghost-bg-hover', snapshots.bellHover.background, snapshots.hoverTokens.ghostHover)
check('bell hover switches the icon to --wb-icon-primary', snapshots.bellHover.color, snapshots.hoverTokens.iconPrimary)
await hover('.user-menu-trigger--workbuddy')
snapshots.triggerHover = await read(`(()=>{const trigger=document.querySelector('.user-menu-trigger--workbuddy');const style=getComputedStyle(trigger);return {hover:trigger.matches(':hover'),background:style.backgroundColor}})()`)
check('account trigger hover uses --wb-bg-hover', snapshots.triggerHover.background, snapshots.hoverTokens.bgHover)
snapshots.colorMixProbe = await read(`(()=>{const probe=document.createElement('div');probe.style.color='color-mix(in srgb, #000 5%, transparent)';document.body.appendChild(probe);const value=getComputedStyle(probe).color;probe.remove();return {probe:value,recoveredBgHover:getComputedStyle(document.querySelector('.user-menu-trigger--workbuddy')).getPropertyValue('--wb-bg-hover'),recoveredGhostHover:getComputedStyle(document.querySelector('.msg-center-bell-btn')).getPropertyValue('--wb-button-ghost-bg-hover')}})()`)
const capture = await main.webContents.debugger.sendCommand('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false })
await fs.writeFile(path.join(directory, 'sidebar-footer.png'), Buffer.from(capture.data, 'base64'))
await step('collapse-lives-in-topbar')
const topbar = await read(`(()=>{const button=document.querySelector('.titlebar__leading [title="收起侧边栏"]');if(!button)return null;const box=button.getBoundingClientRect();return {box:[box.x,box.y,box.width,box.height]}})()`)
check('titlebar still owns the collapse entry', !!topbar, true)
check('titlebar collapse button keeps the 24x24 topbar size', topbar && Math.round(topbar.box[2]) === 24 && Math.round(topbar.box[3]) === 24, true)
await read(`document.querySelector('.titlebar__leading [title="收起侧边栏"]').click()`)
await wait(`!!document.querySelector('.sidebar--collapsed')`, 'sidebar collapsed')
check('titlebar collapse entry still collapses the sidebar', await read(`!!document.querySelector('.sidebar--collapsed')`), true)
check('collapsed rail keeps its own expand entry', await read(`!!document.querySelector('.sidebar__rail [title="展开侧栏"]')`), true)
check('no renderer error logs', rendererErrors, [])
const report = { result: checks.every(item => item.pass) ? 'PASS' : 'FAIL',
scope: 'Real isolated YCode App/preload/IPC/renderer; footer DOM, computed styles, geometry, hover fill and topbar collapse entry measured in the live window.',
sourceEvidence: ['ev_721d830c97114930e046ec8931bd2f473a11ab23efcab9c727c4256889e29879', 'contexts-BkjwYSZT.css', 'UserMenu-CpHz-0rF.css'],
snapshots, checks, rendererErrors }
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
console.log('SIDEBAR_FOOTER_' + report.result + ' ' + directory)
for (const failure of checks.filter(item => !item.pass)) console.log('SIDEBAR_FOOTER_FAILURE ' + JSON.stringify(failure))
app.exit(report.result === 'PASS' ? 0 : 1)
})().catch(async error => {
await fs.writeFile(path.join(directory, 'failure.json'), JSON.stringify({ phase, error: String(error), stack: error.stack, checks, snapshots, rendererErrors }, null, 2))
console.error(error); app.exit(1)
})
}
+15 -29
View File
@@ -439,21 +439,28 @@ export default function Sidebar({ collapsed, onToggleCollapse, activeRoute = 'ch
{hint ? <div className="sidebar__hint">{hint}</div> : null} {hint ? <div className="sidebar__hint">{hint}</div> : null}
{/* ---- 底部账号区(实测:57 高、padding 8 12 8 8、border-top 1px #e6e6e6)---- {/* ---- 底部账号区(原版 .conversation-list-footer:57 高、padding 8 12 8 8、border-top 1px #e6e6e6)----
数据为 mock;收起侧栏入口也放这里 ---- */} 原版该行只有「账号 / 通知 / 小程序」三个入口;收起侧栏位于顶栏 leading 区,不在此处。
数据为 mock(见 TODO 09)。 ---- */}
<div className="sidebar__footer conversation-list-footer"> <div className="sidebar__footer conversation-list-footer">
<div className="user-menu"> <div className="user-menu user-menu-trigger-wrapper">
<button <button
className="user-menu__trigger" className="user-menu-trigger user-menu-trigger--workbuddy"
title={`${MOCK_ACCOUNT.name}(账号为 mock 数据)`} title={`${MOCK_ACCOUNT.name}(账号为 mock 数据)`}
onClick={() => setHint('账号为 mock 数据,尚未接入登录')} onClick={() => setHint('账号为 mock 数据,尚未接入登录')}
> >
<span className="user-menu__avatar">{MOCK_ACCOUNT.initial}</span> <span className="user-menu-trigger-left">
<span className="user-menu__name">{MOCK_ACCOUNT.name}</span> <span className="user-menu-trigger-avatar">
<span className="user-menu-trigger-avatar-img user-menu-avatar-initial">{MOCK_ACCOUNT.initial}</span>
</span>
<span className="user-menu-trigger-name-group">
<span className="user-menu-trigger-name">{MOCK_ACCOUNT.name}</span>
</span>
</span>
</button> </button>
<button <button
className="user-menu__icon-btn" className="msg-center-bell-btn user-menu-trigger-msg-center"
title={unread > 0 ? `通知(${unread} 条未读,mock 数据)` : '通知'} title={unread > 0 ? `通知(${unread} 条未读,mock 数据)` : '通知'}
onClick={() => { onClick={() => {
setNotifOpen((v) => !v) setNotifOpen((v) => !v)
@@ -465,16 +472,12 @@ export default function Sidebar({ collapsed, onToggleCollapse, activeRoute = 'ch
</button> </button>
<button <button
className="user-menu__icon-btn" className="user-menu-trigger-miniprogram"
title="小程序(尚未实现)" title="小程序(尚未实现)"
onClick={() => setHint('「小程序」尚未实现')} onClick={() => setHint('「小程序」尚未实现')}
> >
<IconMiniprogram /> <IconMiniprogram />
</button> </button>
<button className="user-menu__icon-btn" title="收起侧栏" onClick={onToggleCollapse}>
<IconCollapseRail />
</button>
</div> </div>
{notifOpen ? <NotificationsPanel onClose={() => setNotifOpen(false)} /> : null} {notifOpen ? <NotificationsPanel onClose={() => setNotifOpen(false)} /> : null}
@@ -607,20 +610,3 @@ function WorkspaceMoreIcon(): React.JSX.Element { return <WorkspaceSourceIcon na
function WorkspaceEditIcon(): React.JSX.Element { return <WorkspaceSourceIcon name="edit" /> } function WorkspaceEditIcon(): React.JSX.Element { return <WorkspaceSourceIcon name="edit" /> }
function WorkspaceRemoveIcon(): React.JSX.Element { return <WorkspaceSourceIcon name="remove" /> } function WorkspaceRemoveIcon(): React.JSX.Element { return <WorkspaceSourceIcon name="remove" /> }
/** 收起侧栏(自绘,可长期保留) */
function IconCollapseRail(): React.JSX.Element {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.2"
>
<rect x="1.6" y="2.6" width="12.8" height="10.8" rx="2" />
<path d="M6.2 2.6v10.8" />
<path d="M9.6 6.4 8 8l1.6 1.6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
}
+160 -67
View File
@@ -475,97 +475,190 @@
pointer-events: none; pointer-events: none;
} }
/* 底部账号区(实测 57 高、padding 8 12 8 8) */ /* ---- 底部账号区:逐条回收原版规则 ----
.sidebar__footer { contexts-BkjwYSZT.css: .conversation-list-footer{border-top:.5px solid var(--wb-color-border-primary);flex-shrink:0;
justify-content:flex-start;align-items:center;padding:8px 12px 8px 8px;display:flex} / .conversation-list-footer .user-menu{width:100%}
UserMenu-CpHz-0rF.css: .user-menu-trigger* / .msg-center-bell-btn / .msg-center-badge
运行时光栅证据 ev_721d830c97114930e046ec8931bd2f473a11ab23efcab9c727c4256889e29879:
容器 [0,845,283.28,57]、padding 8/12/8/8、border-top 1px rgb(230,230,230);行内 [8,854,263.28,40];
账号触发器 191.28×40、铃铛/小程序各 32×32、头像 28×28、姓名 12/600/12(原版该行只有账号 / 通知 / 小程序三个入口)。 */
.sidebar__footer.conversation-list-footer {
border-top: var(--yc-border-width) solid var(--wb-color-border-primary, var(--yc-border-primary));
flex-shrink: 0; flex-shrink: 0;
height: var(--yc-sidebar-footer-height); justify-content: flex-start;
padding: var(--yc-space-3) var(--yc-space-4) var(--yc-space-3) var(--yc-space-3);
border-top: var(--yc-border-width) solid var(--yc-border-primary);
display: flex;
align-items: center; align-items: center;
padding: var(--yc-space-3) var(--yc-space-4) var(--yc-space-3) var(--yc-space-3);
display: flex;
} }
.user-menu { .conversation-list-footer .user-menu {
display: flex;
align-items: center;
gap: var(--yc-space-2);
width: 100%; width: 100%;
height: var(--yc-user-menu-height); }
.user-menu-trigger-wrapper {
align-items: center;
gap: 4px;
width: 100%;
display: flex;
position: relative; position: relative;
} }
.user-menu__trigger { .user-menu-trigger {
cursor: pointer;
background: 0 0;
border: none;
border-radius: 50%;
justify-content: center;
align-items: center;
padding: 4px;
transition: background-color .2s;
display: flex; display: flex;
}
.user-menu-trigger:focus {
outline: none;
}
.user-menu-trigger--workbuddy {
box-sizing: border-box;
border-radius: 8px;
flex: 1;
min-width: 0;
min-height: 40px;
padding: 6px 8px;
overflow: hidden;
}
.user-menu-trigger--workbuddy:hover {
background-color: var(--wb-bg-hover);
}
.user-menu-trigger-left {
flex: 1;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
flex: 1;
min-width: 0; min-width: 0;
height: var(--yc-user-menu-height); display: flex;
padding: 6px var(--yc-space-3);
border-radius: var(--yc-radius-lg);
text-align: left;
} }
.user-menu__trigger:hover { .user-menu-trigger-avatar {
background: var(--yc-bg-hover); flex-shrink: 0;
} justify-content: center;
align-items: center;
.user-menu__avatar { display: inline-flex;
width: var(--yc-user-avatar-size);
height: var(--yc-user-avatar-size);
flex: none;
display: grid;
place-items: center;
border-radius: var(--yc-radius-full);
background: var(--yc-border-secondary);
color: var(--yc-text-primary);
font-size: var(--yc-text-caption-size);
font-weight: var(--yc-font-weight-500);
}
.user-menu__name {
flex: 1;
min-width: 0;
font-size: var(--yc-text-caption-size);
font-weight: var(--yc-font-weight-600);
line-height: var(--yc-text-caption-size);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-menu__icon-btn {
flex: none;
width: var(--yc-icon-btn-md);
height: var(--yc-icon-btn-md);
display: grid;
place-items: center;
border-radius: var(--yc-radius-md);
color: var(--yc-text-secondary);
position: relative; position: relative;
} }
.user-menu__icon-btn:hover { .user-menu-trigger-avatar .user-menu-trigger-avatar-img {
background: var(--yc-bg-hover); object-fit: cover;
border-radius: 50%;
width: 28px;
height: 28px;
display: block;
} }
/* 未读角标(实测 .msg-center-badge:14×14、#f64041、9px) */ /* 原版 .user-menu-avatar-initial 只给底色;YCode 无账号头像图片,用同款字面头像占位。 */
.msg-center-badge { .user-menu-trigger-avatar .user-menu-avatar-initial {
position: absolute; background-color: #0056d4;
top: calc(-1 * var(--yc-badge-offset));
right: calc(-1 * var(--yc-badge-offset));
min-width: var(--yc-badge-size);
height: var(--yc-badge-size);
padding: 0 3px;
border-radius: calc(var(--yc-badge-size) / 2);
background: var(--yc-red-8);
color: var(--yc-white-100); color: var(--yc-white-100);
font-size: 9px; font-size: 12px;
font-weight: var(--yc-font-weight-500); font-weight: var(--yc-font-weight-500);
line-height: var(--yc-badge-size); line-height: 28px;
text-align: center; text-align: center;
} }
.user-menu-trigger-name-group {
flex-direction: column;
flex: 1;
justify-content: center;
align-items: flex-start;
gap: 4px;
width: 100%;
min-width: 0;
height: 28px;
display: flex;
overflow: hidden;
}
.user-menu-trigger-name {
width: 100%;
max-width: 100%;
color: var(--wb-color-text-primary, var(--yc-text-primary));
text-align: left;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 12px;
font-weight: 600;
line-height: 12px;
display: block;
overflow: hidden;
}
.msg-center-bell-btn {
width: 32px;
height: 32px;
color: var(--wb-color-text-secondary, var(--yc-text-secondary));
cursor: pointer;
background: 0 0;
border: none;
border-radius: 6px;
justify-content: center;
align-items: center;
padding: 0;
transition: background-color .15s, color .15s;
display: inline-flex;
position: relative;
}
.msg-center-bell-btn:hover {
background-color: var(--wb-button-ghost-bg-hover);
color: var(--wb-icon-primary);
}
.user-menu-trigger-msg-center {
flex-shrink: 0;
}
.user-menu-trigger-miniprogram {
cursor: pointer;
width: 32px;
height: 32px;
color: var(--wb-color-text-secondary, var(--yc-text-secondary));
background: 0 0;
border: none;
border-radius: 6px;
flex-shrink: 0;
justify-content: center;
align-items: center;
padding: 0;
transition: background-color .15s, color .15s;
display: inline-flex;
}
.user-menu-trigger-miniprogram:hover {
background-color: var(--wb-button-ghost-bg-hover);
color: var(--wb-icon-primary);
}
/* 未读角标(原版 .msg-center-badge:14×14、radius 7、9/500/14、top 2 right 2、translate(40%,-30%)) */
.msg-center-badge {
background: var(--wb-status-error, var(--yc-red-8));
min-width: 14px;
height: 14px;
color: var(--wb-text-on-primary, var(--yc-white-100));
text-align: center;
pointer-events: none;
border-radius: 7px;
padding: 0 3px;
font-size: 9px;
font-weight: 500;
line-height: 14px;
position: absolute;
top: 2px;
right: 2px;
transform: translate(40%, -30%);
}
/* 收起态窄轨 */ /* 收起态窄轨 */
.sidebar--collapsed { .sidebar--collapsed {
align-items: center; align-items: center;
@@ -81,7 +81,8 @@ export const SOURCE_PANEL_LAYOUT_EVIDENCE = {
} }
} as const } as const
export const Rw = {EXPANDED_WIDTH:280,MIN_WIDTH:240,MAX_WIDTH:360,COLLAPSE_THRESHOLD:120,EXPAND_THRESHOLD:150,COLLAPSED_WIDTH:48} as const /** 展开宽度:原版 .conversation-list 运行时 border-box 284.28px、border-right 1px(ev_721d830c…),取 284。 */
export const Rw = {EXPANDED_WIDTH:284,MIN_WIDTH:240,MAX_WIDTH:360,COLLAPSE_THRESHOLD:120,EXPAND_THRESHOLD:150,COLLAPSED_WIDTH:48} as const
export const zw = {MIN_WIDTH:360,PREFERRED_WIDTH:600} as const export const zw = {MIN_WIDTH:360,PREFERRED_WIDTH:600} as const
export const Bw = {BREAKPOINT:600,CHAT_MIN_WIDTH:360} as const export const Bw = {BREAKPOINT:600,CHAT_MIN_WIDTH:360} as const
export const Vw = {MIN_WIDTH:360,DEFAULT_WIDTH:500,OVERVIEW_ONLY_WIDTH:318,MAX_WIDTH:1/0} as const export const Vw = {MIN_WIDTH:360,DEFAULT_WIDTH:500,OVERVIEW_ONLY_WIDTH:318,MAX_WIDTH:1/0} as const