feat(account): align signed-out sidebar menu with design
This commit is contained in:
1 parent
4283f62d01
commit
c062c615dc
12 files changed
+103
-18
No files matched your search
@@ -0,0 +1,4 @@
|
||||
- menu:
|
||||
- menuitem "设置"
|
||||
- menuitem "联系我们"
|
||||
- menuitem "登录"
|
||||
@@ -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 @@
|
||||
设置联系我们登录
|
||||
Reference in new issue
Block a user