feat(account): align signed-out sidebar menu with design

This commit is contained in:
lsdsjy committed 2026-09-23 11:13:27 +08:00
1 parent 4283f62d01
commit c062c615dc
12 files changed
+103 -18

No files matched your search

@@ -0,0 +1,4 @@
- menu:
- menuitem "设置"
- menuitem "联系我们"
- menuitem "登录"
+28 -3
View File
@@ -11,6 +11,7 @@ import {
import { openSettings, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/onboarding-native', import.meta.url))
const SIGNED_OUT_MENU_EXPECTED = join(SNAPSHOT_DIR, 'signed-out-menu.expected.md')
const MODE = webSnapshotMode()
describe.skipIf(MODE === 'record').each([false, true])('web e2e: native credential onboarding (desktop marker: %s)', (desktop) => {
@@ -53,8 +54,32 @@ describe.skipIf(MODE === 'record').each([false, true])('web e2e: native credenti
acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
}
const accountMenu = page.getByRole('button', { name: '账号菜单', exact: true })
if (desktop) await accountMenu.waitFor()
else {
if (desktop) {
await accountMenu.waitFor()
expect(await accountMenu.textContent()).toBe('更多')
const triggerBox = (await accountMenu.boundingBox())!
expect(Math.abs(triggerBox.height - 32)).toBeLessThan(1)
await accountMenu.click()
const menu = page.getByRole('menu')
await menu.waitFor()
expect(await menu.getByRole('menuitem').allTextContents()).toEqual(['设置', '联系我们', '登录'])
const menuBox = (await menu.boundingBox())!
expect(Math.abs(menuBox.width - 124)).toBeLessThan(1)
expect(Math.abs(menuBox.height - 128)).toBeLessThan(1)
expect(Math.abs(triggerBox.y - (menuBox.y + menuBox.height) - 4)).toBeLessThan(1)
for (const row of await menu.getByRole('menuitem').all()) {
const rowBox = (await row.boundingBox())!
expect(Math.abs(rowBox.height - 40)).toBeLessThan(1)
expect(Math.abs(menuBox.width - rowBox.width - 8)).toBeLessThan(1)
const glyph = (await row.locator('svg').first().boundingBox())!
expect(Math.abs(glyph.width - 16)).toBeLessThan(1)
expect(Math.abs(glyph.height - 16)).toBeLessThan(1)
}
const menuAria = await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(SIGNED_OUT_MENU_EXPECTED, menuAria, MODE)
await page.keyboard.press('Escape')
await menu.waitFor({ state: 'detached' })
} else {
await page.getByRole('button', { name: '设置', exact: true }).waitFor()
expect(await accountMenu.count()).toBe(0)
expect(await page.getByRole('dialog', { name: '开始你的创作' }).count()).toBe(0)
@@ -72,6 +97,6 @@ describe.skipIf(MODE === 'record').each([false, true])('web e2e: native credenti
}
expect(tripwire.warnings).toEqual([])
expect(tripwire.pageErrors).toEqual([])
await assertFixtureInventory(SNAPSHOT_DIR, ['models.expected.md'])
await assertFixtureInventory(SNAPSHOT_DIR, ['models.expected.md', 'signed-out-menu.expected.md'])
})
})
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-settings-account/README.md
README.md: 2eb0f56daec2ad66c1407e397e30a872bda8c52e
README.zh.md: f1c1a5b66327d9dee85536afb737efd2afa23acb
README.md: 55a747896f5c115d08ebacea6046b4de711a530c
README.zh.md: 582a1b21113631305eb38bd02a140479704d365d
@@ -24,7 +24,7 @@ Desktop Account settings display DeepSeek login state and offer browser authoriz
The client activates only inside Desktop, identified by its preload bridge. Plain Web clients keep the standard Settings launcher and API-key onboarding without account login, account settings, or an account-state subscription.
The sidebar and Account settings display the profile avatar as a circular image, with the account icon as fallback when the URL is absent or the image fails to load. The collapsed sidebar centers the avatar in a 36 × 36 px button.
The sidebar and Account settings display the profile avatar as a circular image, with the account icon as fallback when the URL is absent or the image fails to load. The collapsed sidebar centers the avatar in a 36 × 36 px button. Before sign-in the sidebar launcher shows a More row instead, leading with the ellipsis glyph.
The section registers through settings.section and uses the account Remote namespace. The stream survives carrier reconnects through the shared Remote supervisor. The page renders feature-owned English and Chinese copy and keeps API keys separate from account state.
@@ -34,7 +34,7 @@ Desktop usage and top-up actions open an isolated native Platform view below a 4
The account menu's Feedback entry opens the Feishu questionnaire in the system browser. It supplies the available build version, UI locale and physical screen resolution as prefill_* parameters, with hide_*=1 for every context field; the account UID, tokens and contact details are excluded. Configure contactFormUrl on the ui-settings-account plugin to select another HTTPS form. contactSource defaults to empty until the questionnaire supports a Harness source option; OS and device fields remain unfilled, matching the Web implementation.
The sidebar account menu uses the shared Menu surface, backdrop blur, spacing, and row typography; feature styles only size its launcher.
The sidebar account menu uses the shared Menu surface and backdrop blur. The signed-in menu keeps the shared row typography; the signed-out menu carries its own wider rows and its Contact us copy for the Feedback entry.
The account card’s More account information link opens `https://platform.deepseek.com` in the system browser.
@@ -24,7 +24,7 @@ Desktop 设置中的账号页面显示 DeepSeek 登录状态,并提供浏览
客户端仅在具有 preload 桥接的 Desktop 内启用。普通 Web 客户端保留标准设置入口和 API Key 引导,不显示账号登录和账号设置,也不订阅账号状态。
侧边栏和账号设置以圆形图片显示账号头像;头像 URL 缺失或图片加载失败时使用账号图标兜底。折叠侧边栏将头像居中放置在 36 × 36 px 的按钮内。
侧边栏和账号设置以圆形图片显示账号头像;头像 URL 缺失或图片加载失败时使用账号图标兜底。折叠侧边栏将头像居中放置在 36 × 36 px 的按钮内。登录前侧边栏入口改为「更多」行,以三点图标开头。
该页面通过 settings.section 注册,并使用 account Remote 命名空间。共享 Remote 管理器负责连接恢复后的流重连。页面使用功能自有的中英文文案,将 API Key 与账号状态分开。
@@ -34,7 +34,7 @@ Desktop 的用量和充值操作在 48px 返回栏下方打开隔离的原生 Pl
账号菜单的“意见反馈”入口在系统浏览器中打开飞书问卷。链接通过 prefill_* 参数填写已有的构建版本、界面语言和屏幕物理分辨率,并为所有上下文字段设置 hide_*=1;不传账号 UID、token 或联系方式。可在 ui-settings-account 插件配置 contactFormUrl,切换到另一个 HTTPS 问卷。问卷支持 Harness 来源选项前,contactSource 默认为空;OS 和设备字段沿用 Web 实现,保持未填写。
侧边栏账号菜单使用公共 Menu 的表面、背景模糊、间距和菜单项排版;功能样式仅设置入口布局。
侧边栏账号菜单使用公共 Menu 的表面和背景模糊。已登录菜单沿用公共菜单项排版;未登录菜单使用自己的较宽菜单项,并将“意见反馈”入口的文案改为“联系我们”。
账号卡片的“更多账号信息”链接通过系统浏览器打开 `https://platform.deepseek.com`。
@@ -6,3 +6,32 @@
.avatar { display: flex; align-items: center; justify-content: center; flex: none; width: 32px; height: 32px; border-radius: 16px; background: var(--dsw-alias-bg-skeleton); color: var(--dsw-alias-label-tertiary); }
.label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.error { color: var(--dsw-alias-state-error-primary); font-size: 12px; }
/* Signed-out launcher: a 32px More row carrying the 14px ellipsis. */
.trigger[data-signed-out='true']:not([data-collapsed='true']) { height: 32px; padding: 6px 2px 6px 6px; line-height: 20px; }
/* Signed-out menu card and rows; listClassName reaches the portaled card. */
.signedOutMenu[role='menu'] {
min-width: 124px;
max-width: 280px;
padding: 4px;
border-radius: 12px;
--dsw-elevation-stroke-color: var(--dsw-alias-border-inverted);
}
.signedOutMenu [role='menuitem'] {
min-height: 40px;
gap: 8px;
padding: 8px 10px;
border-radius: 10px;
font-size: 14px;
line-height: 22px;
}
.signedOutMenu [role='menuitem'] > span:first-child:not(:last-child) {
width: 16px;
height: 16px;
color: var(--dsw-alias-label-primary);
}
.signedOutMenu [role='menuitem'] > span:first-child:not(:last-child) svg { width: 16px; height: 16px; }
@@ -1,6 +1,8 @@
/** Sidebar account launcher and locally authoritative sign-out action. */
import { useEffect, useState } from 'react'
import { Toast, Menu, IconPaperPlaneOutlineMedium, IconSettingsOutlineMedium, IconUserOutlineMedium } from '@deepseek-ai/dsh-client-ui-primitives'
import {
Toast, Menu, IconEllipsisOutlineMedium, IconPaperPlaneOutlineMedium, IconSettingsOutlineMedium, IconUserOutlineMedium,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type { AccountSectionInjected } from './AccountSection.tsx'
import { SignOutDialog } from './SignOutDialog.tsx'
@@ -41,15 +43,17 @@ export function AccountMenu({
const beginSignIn = (): void => { setOpen(false); void start().catch(() => undefined) }
return <div className={css.root}>
{expiryNotice && <Toast text={t('sessionExpired')} onDone={() => { setExpiryNotice(false) }} />}
<Menu open={open} side="top" portal autoFocus className={css.anchor}
anchor={<button type="button" className={css.trigger} data-collapsed={!wide} aria-label={t('menu')}
<Menu open={open} side="top" portal autoFocus className={css.anchor} listClassName={signedIn ? undefined : css.signedOutMenu}
anchor={<button type="button" className={css.trigger} data-collapsed={!wide} data-signed-out={!signedIn} aria-label={t('menu')}
aria-haspopup="menu" aria-expanded={open} onClick={() => { setOpen(value => !value) }}>
<span className={css.avatar}><AccountAvatar url={signedIn && profile?.status === 'ready' ? profile.value.avatarUrl : null} /></span>
{wide && <span className={css.label}>{signedIn ? label : t('signedOut')}</span>}
{signedIn
? <span className={css.avatar}><AccountAvatar url={profile?.status === 'ready' ? profile.value.avatarUrl : null} /></span>
: <IconEllipsisOutlineMedium size={14} />}
{wide && <span className={css.label}>{signedIn ? label : t('more')}</span>}
</button>}
items={[
{ id: 'settings', label: t('settings'), icon: <IconSettingsOutlineMedium size={16} /> },
{ id: 'contact', label: t('contactUs'), icon: <IconPaperPlaneOutlineMedium size={16} /> },
{ id: 'contact', label: signedIn ? t('contactUs') : t('contactUsSignedOut'), icon: <IconPaperPlaneOutlineMedium size={16} /> },
...(signedIn ? [{ id: 'signout', label: t('signOut'), icon: <LogoutIcon />, disabled: busy }]
: [{ id: 'signin', label: t('signIn'), icon: <IconUserOutlineMedium size={16} /> }]),
]}
@@ -9,7 +9,7 @@ export const en = {
failureTitle: 'Could not sign in',
platformFailed: 'Could not complete the operation. Try again.', platformRetry: 'Retry',
loading: 'Loading…', backToHarness: 'Back to DeepSeek Harness',
settings: 'Settings', contactUs: 'Feedback', menu: 'Account menu',
settings: 'Settings', contactUs: 'Feedback', contactUsSignedOut: 'Contact us', menu: 'Account menu',
nav: 'Account', signedIn: 'Signed in to DeepSeek', signedOut: 'Not signed in',
signIn: 'Sign in', signOut: 'Sign out',
signOutUnknownDescription: 'Could not check running tasks. Signing out may interrupt tasks using this account. Sign out now?',
@@ -38,7 +38,7 @@ export const zh: Record<AccountKey, string> = {
failureTitle: '登录失败',
platformFailed: '操作未完成,请重试', platformRetry: '重试',
loading: '加载中…', backToHarness: '返回 DeepSeek Harness',
settings: '设置', contactUs: '意见反馈', menu: '账号菜单',
settings: '设置', contactUs: '意见反馈', contactUsSignedOut: '联系我们', menu: '账号菜单',
nav: '账号与余额', signedIn: '已登录 DeepSeek', signedOut: '尚未登录',
signIn: '登录', signOut: '退出登录',
signOutUnknownDescription: '暂时无法确认任务状态。退出登录可能会中断使用此账号的任务,是否继续?',
@@ -95,6 +95,27 @@ it.each([en, zh].flatMap(copy => ([false, true, 'unknown'] as const).map(running
expect(screen.queryByRole('menu')).toBeNull()
})
it.each([en, zh])('offers settings, contact and sign-in from the signed-out account menu', async (copy) => {
const openSettings = vi.fn()
const operations = operationsOf({ status: 'signed-out', attempt: null })
const { AccountMenu } = await import('../src/client/AccountMenu.tsx')
render(<AccountMenu {...({} as GlobalStandardProps)} {...operations}
useAccount={selector => selector(operations.hooks.account.getSnapshot())}
useTheme={selector => selector(operations.hooks.theme.getSnapshot())} wide openOnboarding={() => {}} openSettings={openSettings}
t={key => key in copy ? copy[key as AccountKey] : key} />)
const trigger = screen.getByRole('button', { name: copy.menu })
expect(trigger.textContent).toBe(copy.more)
expect(trigger.querySelector('svg')).not.toBeNull()
fireEvent.click(trigger)
expect(screen.getAllByRole('menuitem').map(item => item.textContent)).toEqual([copy.settings, copy.contactUsSignedOut, copy.signIn])
await expect(`${screen.getByRole('menu').textContent}\n`).toMatchFileSnapshot(`./expected/menu-signed-out-${copy === en ? 'en' : 'zh'}.txt`)
fireEvent.click(screen.getByRole('menuitem', { name: copy.settings }))
expect(openSettings).toHaveBeenCalledOnce()
fireEvent.click(screen.getByRole('button', { name: copy.menu }))
fireEvent.click(screen.getByRole('menuitem', { name: copy.contactUsSignedOut }))
expect(operations.contactUs).toHaveBeenCalledOnce()
})
it('reports a failed start in the login dialog, not as a sidebar alert', async () => {
const operations = mount({ status: 'signed-out', attempt: null })
cleanup()
@@ -1 +1 @@
Not signed inCould not sign inCould not complete the operation. Try again.Add API KeySign in again
MoreCould not sign inCould not complete the operation. Try again.Add API KeySign in again
@@ -0,0 +1 @@
SettingsContact usSign in
@@ -0,0 +1 @@
设置联系我们登录