feat(sidebar): replicate WorkBuddy account menu (TODO 09)

This commit is contained in:
xiaohan.yuan committed 2026-10-17 07:57:17 +08:00
1 parent 6173a77918
commit cb8a7e6466
9 files changed
+1035 -6

No files matched your search

+23 -1
View File
@@ -22,6 +22,7 @@ import {
import { IconSearch } from './icons-extra'
import NotificationsPanel from './NotificationsPanel'
import WorkspaceDialog from './WorkspaceDialog'
import { AccountMenu } from '../workbuddy/user-menu/AccountMenu'
import { TaskContextMenu } from './task-menu'
import { IconTaskMore as TaskMoreIcon, IconTaskPinBadge as TaskPinIcon } from './task-menu-icons'
import type { TaskMenuAction, TaskMenuAnchor } from './task-menu'
@@ -116,6 +117,7 @@ export default function Sidebar({ collapsed, onToggleCollapse, activeRoute = 'ch
const taskStartLock = useRef(false)
const [notifOpen, setNotifOpen] = useState(false)
const [unread, setUnread] = useState(MOCK_UNREAD_COUNT)
const [accountMenu, setAccountMenu] = useState<{ left: number; top: number; trigger: HTMLElement | null } | null>(null)
const [hint, setHint] = useState<string | null>(null)
const hintTimer = useRef<number | null>(null)
const searchRef = useRef<HTMLInputElement>(null)
@@ -126,6 +128,8 @@ export default function Sidebar({ collapsed, onToggleCollapse, activeRoute = 'ch
window.addEventListener('keydown', close)
return () => window.removeEventListener('keydown', close)
}, [spaceMenu])
// 收起侧栏时不存在触发器,账户菜单随之关闭(原版触发器就在该行内)。
useEffect(() => { if (collapsed) setAccountMenu(null) }, [collapsed])
useLayoutEffect(() => {
if (!spaceMenu || !spaceMenuRef.current) return
const rect = spaceMenuRef.current.getBoundingClientRect()
@@ -447,7 +451,13 @@ export default function Sidebar({ collapsed, onToggleCollapse, activeRoute = 'ch
<button
className="user-menu-trigger user-menu-trigger--workbuddy"
title={`${MOCK_ACCOUNT.name}(账号为 mock 数据)`}
onClick={() => setHint('账号为 mock 数据,尚未接入登录')}
aria-haspopup="menu"
aria-expanded={accountMenu !== null}
onClick={(event) => {
const rect = event.currentTarget.getBoundingClientRect()
setNotifOpen(false)
setAccountMenu((current) => current ? null : { left: rect.left, top: rect.top, trigger: event.currentTarget })
}}
>
<span className="user-menu-trigger-left">
<span className="user-menu-trigger-avatar">
@@ -481,6 +491,18 @@ export default function Sidebar({ collapsed, onToggleCollapse, activeRoute = 'ch
</div>
{notifOpen ? <NotificationsPanel onClose={() => setNotifOpen(false)} /> : null}
{accountMenu ? <AccountMenu
anchor={accountMenu}
name={MOCK_ACCOUNT.name}
initial={MOCK_ACCOUNT.initial}
onClose={() => setAccountMenu(null)}
onOpenSettings={(section) => {
setAccountMenu(null)
if (window.ycode.app.openSettings) void window.ycode.app.openSettings(section)
}}
onUnavailable={(message) => { setAccountMenu(null); setHint(message) }}
/> : null}
</div>
</aside>
)
@@ -0,0 +1,156 @@
/**
* 账户菜单(TODO 09)。
*
* 结构、几何与样式来自 REA ev_95a39af88081a641961779c55d5855e0e3d57889e5d2ccad2c3dc15e26e1c8bb
* (WorkBuddy 5.8.3,账户菜单打开状态,视口 1336×1240):
* 根节点 [8,607,320,577]、placement-top(底边距触发器上沿 8px、左边缘与触发器对齐、宽 320)、
* 头部 56 高(头像 40×40)、计费摘要两个 141×86 单元、条目 32 高 + footer 顶部 1px。
*
* 内容按用户指定范围:头像与名称、套餐、余额、升级套餐、购买积分、设置、外观、记忆与进化、
* 检查更新、退出登录(不逐项照搬原版的 Buddy加油站/去邀约/成长计划/意见反馈)。
* YCode 没有账号与计费服务:套餐/余额/升级套餐/购买积分/退出登录/记忆与进化/检查更新
* 明确标注未接入,不用虚构数值或成功提示代替真实能力;设置与外观接入真实设置窗口。
*/
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { IconAccountAppearance, IconAccountLogout, IconAccountMemory, IconAccountSettings, IconAccountUpdate } from './account-menu-icons'
import './account-menu-source.css'
export interface AccountMenuAnchor {
/** 触发器左边缘(原版 popover 左边缘与触发器对齐) */
left: number
/** 触发器上边缘(原版 popover 底边距其 8px) */
top: number
/** 触发器元素:外部点击判定与 Escape 后焦点返回都用它 */
trigger: HTMLElement | null
}
export interface AccountMenuProps {
anchor: AccountMenuAnchor
name: string
initial: string
onClose(): void
onOpenSettings(section: 'model' | 'appearance'): void
onUnavailable(message: string): void
}
export function AccountMenu({ anchor, name, initial, onClose, onOpenSettings, onUnavailable }: AccountMenuProps): React.JSX.Element {
const ref = useRef<HTMLDivElement>(null)
const [position, setPosition] = useState<{ left: number; bottom: number }>({ left: anchor.left, bottom: 0 })
const [theme, setTheme] = useState<'light' | 'dark'>(() => document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light')
// 「外观」行显示当前主题;主题切换由 useSharedTheme 写在 documentElement.dataset.theme 上。
useEffect(() => {
const update = (): void => setTheme(document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light')
const observer = new MutationObserver(update)
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] })
return () => observer.disconnect()
}, [])
// 关闭由文档级监听完成:REA 证据 ev_95a39af8… 的 736 个节点里没有遮罩层节点,
// 因此不做全屏 overlay(那会吃掉外部点击的第一次交互),与 SourceSelectPopover 同款约定。
useEffect(() => {
const outside = (event: PointerEvent): void => {
if (event.button !== 0 || !(event.target instanceof Node)) return
if (ref.current?.contains(event.target) || anchor.trigger?.contains(event.target)) return
onClose()
}
const escape = (event: KeyboardEvent): void => {
if (event.key !== 'Escape') return
event.preventDefault(); event.stopPropagation(); onClose()
anchor.trigger?.focus({ preventScroll: true })
}
document.addEventListener('pointerdown', outside)
document.addEventListener('keydown', escape, true)
return () => { document.removeEventListener('pointerdown', outside); document.removeEventListener('keydown', escape, true) }
}, [anchor.trigger, onClose])
// 原版为 placement-top:先按锚点落位,再按视口夹紧(与任务菜单同一套做法)。
useLayoutEffect(() => {
const element = ref.current
if (!element) return
const place = (): void => {
const rect = element.getBoundingClientRect()
const reference = anchor.trigger?.getBoundingClientRect()
const left = Math.max(4, Math.min(reference?.left ?? anchor.left, window.innerWidth - rect.width - 4))
const bottom = Math.max(4, window.innerHeight - (reference?.top ?? anchor.top) + 8)
setPosition({ left, bottom })
}
place()
window.addEventListener('resize', place)
return () => window.removeEventListener('resize', place)
}, [anchor])
return createPortal(
<div className="workspace-source ycode-account-menu-scope">
<div ref={ref} role="menu" aria-label="账户菜单"
className="wb-popover wb-popover--placement-top user-menu-popover user-menu-popover--local user-menu-popover--portal ycode-account-menu"
style={{ position: 'fixed', left: position.left, bottom: position.bottom }}
onClick={(event) => event.stopPropagation()}>
<section className="user-menu-section">
<div className="user-menu-header">
<div className="user-menu-header-avatar"><span className="user-menu-avatar-initial">{initial}</span></div>
<div className="user-menu-header-info">
<div className="user-menu-header-name"><span className="user-menu-header-name-text">{name}</span></div>
<div className="user-menu-header-subtitle">账号为 mock 数据,尚未接入登录</div>
</div>
</div>
</section>
<section className="user-menu-section user-menu-billing-summary">
{[{ title: '套餐', action: '升级套餐' }, { title: '余额', action: '购买积分' }].map((cell) => (
<div className="user-menu-billing-cell" key={cell.title}>
<div className="user-menu-billing-cell-title">{cell.title}</div>
<div className="user-menu-billing-cell-value"><span className="user-menu-billing-cell-value-main">未接入</span></div>
<div className="user-menu-billing-cell-action">
<button type="button" className="wb-button" disabled title="尚未接入账号与计费服务">{cell.action}</button>
</div>
</div>
))}
</section>
<section className="user-menu-section">
<div className="user-menu-items">
<button type="button" className="user-menu-item" role="button" onClick={() => onOpenSettings('model')}>
<span className="user-menu-item-icon"><IconAccountSettings /></span>
<span className="user-menu-item-label">设置</span>
<span className="user-menu-item-value"><span className="user-menu-item-shortcut">Ctrl+,</span></span>
</button>
<button type="button" className="user-menu-item user-menu-item--appearance" role="button" onClick={() => onOpenSettings('appearance')}>
<span className="user-menu-item-icon"><IconAccountAppearance /></span>
<span className="user-menu-item-label">外观</span>
<span className="user-menu-item-value">{theme === 'dark' ? '深色' : '浅色'}</span>
</button>
<button type="button" aria-disabled="true" title="记忆与进化尚未拆解"
className="user-menu-item user-menu-item--pending user-menu-item--disabled" role="button"
onClick={() => onUnavailable('「记忆与进化」尚未接入(待拆解)')}>
<span className="user-menu-item-icon"><IconAccountMemory /></span>
<span className="user-menu-item-label">记忆与进化</span>
<span className="user-menu-item-value">待拆解</span>
</button>
<button type="button" aria-disabled="true" title="尚未接入更新检查"
className="user-menu-item user-menu-item--pending user-menu-item--disabled" role="button"
onClick={() => onUnavailable('「检查更新」尚未接入')}>
<span className="user-menu-item-icon"><IconAccountUpdate /></span>
<span className="user-menu-item-label">检查更新</span>
<span className="user-menu-item-value">未接入</span>
</button>
</div>
<div className="user-menu-footer">
<button type="button" aria-disabled="true" title="尚未接入账号服务"
className="user-menu-item user-menu-item--logout user-menu-item--pending user-menu-item--disabled" role="button"
onClick={() => onUnavailable('尚未接入账号服务,无法退出登录')}>
<span className="user-menu-item-icon"><IconAccountLogout /></span>
<span className="user-menu-item-label">退出登录</span>
<span className="user-menu-item-value">未接入</span>
</button>
</div>
</section>
</div>
</div>,
document.body
)
}
export default AccountMenu
@@ -0,0 +1,80 @@
/**
* 账户菜单条目图标。
*
* 说明:WorkBuddy 账户菜单条目的原始 SVG 路径尚未通过 REA 回收(本轮只采集了菜单的
* 结构、几何与计算样式,见 docs/复刻待办清单-2026-10-17.md TODO 09)。这里先用
* Lucide(MIT)同族线性图标的路径占位,几何尺寸按原版 16×16 对齐;
* 回收图标后应替换为原版路径,或在正式发布前替换为自有图标集。
*/
import type { SVGProps } from 'react'
type IconProps = SVGProps<SVGSVGElement>
function StrokeIcon({ children, ...props }: IconProps & { children: React.ReactNode }): React.JSX.Element {
return (
<svg {...props} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
{children}
</svg>
)
}
/** 设置 */
export function IconAccountSettings(props: IconProps): React.JSX.Element {
return (
<StrokeIcon {...props}>
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
<circle cx="12" cy="12" r="3" />
</StrokeIcon>
)
}
/** 外观 */
export function IconAccountAppearance(props: IconProps): React.JSX.Element {
return (
<StrokeIcon {...props}>
<circle cx="13.5" cy="6.5" r=".5" />
<circle cx="17.5" cy="10.5" r=".5" />
<circle cx="8.5" cy="7.5" r=".5" />
<circle cx="6.5" cy="12.5" r=".5" />
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z" />
</StrokeIcon>
)
}
/** 记忆与进化 */
export function IconAccountMemory(props: IconProps): React.JSX.Element {
return (
<StrokeIcon {...props}>
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z" />
<path d="M20 3v4" />
<path d="M22 5h-4" />
<path d="M4 17v2" />
<path d="M5 18H3" />
</StrokeIcon>
)
}
/** 检查更新 */
export function IconAccountUpdate(props: IconProps): React.JSX.Element {
return (
<StrokeIcon {...props}>
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
<path d="M8 16H3v5" />
</StrokeIcon>
)
}
/** 退出登录 */
export function IconAccountLogout(props: IconProps): React.JSX.Element {
return (
<StrokeIcon {...props}>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<path d="m16 17 5-5-5-5" />
<path d="M21 12H9" />
</StrokeIcon>
)
}
@@ -0,0 +1,377 @@
/* ---- 账户菜单(TODO 09)----
结构与几何来自 REA ev_95a39af88081a641961779c55d5855e0e3d57889e5d2ccad2c3dc15e26e1c8bb
(WorkBuddy 5.8.3 账户菜单打开状态,视口 1336×1240):
根节点 [8,607,320,577]、placement-top(底边距触发器上沿 8px、左边缘与触发器对齐、宽 320);
头部区 [17,616,302,72](.user-menu-header 56 高、头像 40×40);
计费摘要 [17,696,302,102](两个 141×86 单元,title 12/20、value 14/500/22、action 24 高);
条目区 [17,806,302,369](.user-menu-items 内 32 高条目、1px 分隔线、footer 顶部 1px)。
规则逐条取自回收表 UserMenu-CpHz-0rF.css,popover 基类取自 src-Bh3YZ35H.css。
侧栏作用域(.workspace-source)没有 popover/卡片相关 token,按回收 token 的浅/深色取值在此补齐。 */
.ycode-account-menu-scope {
/* 以下 5 个 token 按回收表 tokens-Df-i-zsO.css / src-CBaV_A3S.css 的根作用域取值补齐:
--wb-bg-popover、--wb-popover-radius(--wb-radius-2xl)、--wb-bg-card、--wb-border-subtle、
--wb-scrollbar-thumb、--wb-scrollbar-thumb-hover、--wb-dropdown-item-bg-hover、
--wb-todo-menu-text-view-more(侧栏 .workspace-source 作用域未定义它们)。 */
--wb-bg-popover: var(--wb-palette-white-100);
--wb-popover-padding: 8px;
--wb-radius-2xl: 16px;
--wb-popover-radius: var(--wb-radius-2xl);
--wb-popover-text-color: var(--wb-color-text-primary);
--wb-popover-font-size: var(--wb-font-body-size);
--wb-popover-line-height: var(--wb-font-body-line-height);
--wb-popover-border-color: var(--wb-color-border-solid);
--wb-popover-shadow: 0px 6px 24px 0px #0000000a, 0px 4px 6px 0px #0000000a;
--wb-bg-card: var(--wb-palette-white-100);
--wb-border-subtle: var(--wb-palette-gray-3);
--wb-dropdown-item-bg-hover: var(--wb-color-palette-gray-3, #f2f2f2);
--wb-scrollbar-thumb: var(--wb-palette-black-20);
--wb-scrollbar-thumb-hover: var(--wb-palette-black-30);
--wb-todo-menu-text-view-more: var(--wb-palette-black-50);
}
[data-theme='dark'] .ycode-account-menu-scope {
--wb-bg-popover: #242424;
--wb-bg-card: #242424;
--wb-border-subtle: color-mix(in srgb, var(--wb-palette-white-100) 6%, transparent);
--wb-dropdown-item-bg-hover: #2e2e2e;
--wb-scrollbar-thumb: var(--wb-palette-white-20);
--wb-scrollbar-thumb-hover: var(--wb-palette-white-30);
--wb-todo-menu-text-view-more: var(--wb-palette-white-50);
--wb-popover-shadow: 0px 6px 24px -4px #0006, 0px 4px 6px -8px #0003;
}
.ycode-account-menu {
z-index: 10000;
}
/* .wb-popover 基类(src-Bh3YZ35H.css) */
.ycode-account-menu-scope .wb-popover {
background: var(--wb-bg-popover);
border-radius: var(--wb-popover-radius);
color: var(--wb-popover-text-color);
padding: var(--wb-popover-padding);
font-size: var(--wb-popover-font-size);
line-height: var(--wb-popover-line-height);
border: .5px solid var(--wb-popover-border-color);
box-shadow: var(--wb-popover-shadow);
outline: none;
flex-direction: column;
display: flex;
}
/* .user-menu-popover(UserMenu-CpHz-0rF.css) */
.ycode-account-menu-scope .user-menu-popover {
/* 原版把 popover 底色在本规则内改写为 --wb-bg-secondary(浅色 rgb(247,247,247) / 深色 #292929) */
--wb-bg-popover: var(--wb-bg-secondary);
--wb-popover-padding: 8px;
--user-menu-hover-bg: var(--wb-bg-hover);
box-sizing: border-box;
-webkit-app-region: no-drag;
scrollbar-width: thin;
scrollbar-color: var(--wb-scrollbar-thumb) transparent;
gap: 8px;
width: max-content;
min-width: 270px;
max-width: 320px;
max-height: calc(100vh - 32px);
overflow: hidden auto;
}
.ycode-account-menu-scope .user-menu-popover::-webkit-scrollbar {
background: 0 0 !important;
width: 6px !important;
height: 6px !important;
}
.ycode-account-menu-scope .user-menu-popover::-webkit-scrollbar-track {
background: 0 0 !important;
border: none !important;
}
.ycode-account-menu-scope .user-menu-popover::-webkit-scrollbar-thumb {
background-color: var(--wb-scrollbar-thumb) !important;
background-clip: padding-box !important;
border: 1px solid #0000 !important;
border-radius: 999px !important;
}
.ycode-account-menu-scope .user-menu-popover::-webkit-scrollbar-thumb:hover {
background-color: var(--wb-scrollbar-thumb-hover) !important;
}
.ycode-account-menu-scope .user-menu-popover::-webkit-scrollbar-corner {
background: 0 0 !important;
}
.ycode-account-menu-scope .user-menu-popover--local {
width: 320px;
max-width: calc(100vw - 24px);
}
.ycode-account-menu-scope .user-menu-section {
background: var(--wb-bg-card);
box-sizing: border-box;
border-radius: 8px;
padding: 8px;
}
/* 头部 */
.ycode-account-menu-scope .user-menu-header {
box-sizing: border-box;
align-items: center;
gap: 8px;
min-height: 56px;
padding: 8px;
display: flex;
}
.ycode-account-menu-scope .user-menu-header-avatar {
flex-shrink: 0;
width: 40px;
height: 40px;
display: flex;
}
/* .user-menu-avatar-initial{background-color:#0056d4}(回收规则)。 */
.ycode-account-menu-scope .user-menu-avatar-initial {
background-color: #0056d4;
}
/* YCode 没有账号头像图片:用初始字当头像,尺寸对齐原版 .user-menu-header-avatar 的 40×40。 */
.ycode-account-menu-scope .user-menu-header-avatar .user-menu-avatar-initial {
width: 40px;
height: 40px;
border-radius: 50%;
color: var(--yc-white-100);
font-size: 14px;
font-weight: 500;
line-height: 40px;
text-align: center;
}
.ycode-account-menu-scope .user-menu-header-info {
flex: 1;
min-width: 0;
}
.ycode-account-menu-scope .user-menu-header-name {
color: var(--wb-color-text-primary);
text-overflow: ellipsis;
white-space: nowrap;
align-items: center;
gap: 4px;
min-width: 0;
font-size: 16px;
font-weight: 500;
line-height: 24px;
display: flex;
overflow: hidden;
}
.ycode-account-menu-scope .user-menu-header-name-text {
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
overflow: hidden;
}
.ycode-account-menu-scope .user-menu-header-subtitle {
color: var(--wb-color-text-tertiary);
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 2px;
font-size: 13px;
line-height: 1.4;
overflow: hidden;
}
/* 计费摘要(套餐 / 余额) */
.ycode-account-menu-scope .user-menu-billing-summary {
align-items: stretch;
gap: 4px;
display: flex;
}
.ycode-account-menu-scope .user-menu-billing-cell {
box-sizing: border-box;
border-radius: 8px;
flex-direction: column;
flex: 1 1 0;
min-width: 0;
padding: 4px 8px 8px;
display: flex;
}
.ycode-account-menu-scope .user-menu-billing-cell-title {
color: var(--wb-color-text-tertiary);
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
line-height: 20px;
overflow: hidden;
}
.ycode-account-menu-scope .user-menu-billing-cell-value {
min-width: 0;
min-height: 22px;
color: var(--wb-color-text-primary);
font-variant-numeric: tabular-nums;
white-space: nowrap;
align-items: center;
gap: 4px;
font-size: 14px;
font-weight: 500;
line-height: 22px;
display: flex;
}
.ycode-account-menu-scope .user-menu-billing-cell-value-main {
text-overflow: ellipsis;
min-width: 0;
overflow: hidden;
}
.ycode-account-menu-scope .user-menu-billing-cell-action {
margin-top: 8px;
}
/* 未接入账号/计费服务:按钮保持原版尺寸,但明确禁用而不是假装可用。 */
.ycode-account-menu .user-menu-billing-cell-action .wb-button {
box-sizing: border-box;
width: 100%;
height: 24px;
border: none;
border-radius: 6px;
justify-content: center;
align-items: center;
font-size: 12px;
line-height: 20px;
display: inline-flex;
}
.ycode-account-menu .user-menu-billing-cell-action .wb-button:disabled {
background: var(--wb-palette-black-20);
color: var(--wb-color-text-disabled);
cursor: default;
}
/* 条目区 */
.ycode-account-menu-scope .user-menu-items {
flex-direction: column;
gap: 2px;
display: flex;
position: relative;
overflow: visible;
}
.ycode-account-menu-scope .user-menu-separator {
border-bottom: 1px solid var(--wb-border-subtle);
border-radius: 0;
margin: 6px 0;
padding: 0;
}
.ycode-account-menu-scope .user-menu-item {
width: 100%;
min-height: 32px;
color: var(--wb-color-text-primary);
cursor: pointer;
text-align: left;
box-sizing: border-box;
background: 0 0;
border: none;
border-radius: 8px;
justify-content: space-between;
align-items: center;
gap: 8px;
margin: 0;
padding: 5px 8px;
font-size: 14px;
line-height: 22px;
transition: background-color .15s;
display: flex;
}
.ycode-account-menu-scope .user-menu-item:hover {
background-color: var(--wb-dropdown-item-bg-hover);
}
.ycode-account-menu-scope .user-menu-item svg {
flex-shrink: 0;
}
.ycode-account-menu-scope .user-menu-item--disabled {
cursor: default;
}
.ycode-account-menu-scope .user-menu-item--disabled:hover {
background-color: #0000;
}
.ycode-account-menu-scope .user-menu-item-icon {
flex-shrink: 0;
justify-content: center;
align-items: center;
width: 16px;
height: 16px;
display: flex;
}
.ycode-account-menu-scope .user-menu-item-icon svg {
width: 16px;
height: 16px;
}
.ycode-account-menu-scope .user-menu-item-label {
flex: 1;
}
.ycode-account-menu-scope .user-menu-item-value {
align-items: center;
display: flex;
}
.ycode-account-menu-scope .user-menu-item-shortcut {
color: var(--wb-color-text-tertiary, #00000080);
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
text-decoration-line: none;
}
.ycode-account-menu-scope .user-menu-item--appearance .user-menu-item-value,
.ycode-account-menu-scope .user-menu-item--pending .user-menu-item-value {
color: var(--wb-todo-menu-text-view-more);
white-space: nowrap;
flex-shrink: 0;
justify-content: flex-end;
gap: 2px;
font-size: 12px;
line-height: 20px;
}
.ycode-account-menu-scope .user-menu-footer {
margin-top: 6px;
padding: 8px 0 0;
position: relative;
}
.ycode-account-menu-scope .user-menu-footer:before {
content: "";
background: var(--wb-border-subtle);
height: 1px;
position: absolute;
top: 0;
left: 0;
right: 0;
}
.ycode-account-menu-scope .user-menu-footer .user-menu-item--logout {
color: var(--wb-color-text-primary);
}
.ycode-account-menu-scope .user-menu-footer .user-menu-item--logout .user-menu-item-label {
align-items: center;
display: flex;
}