fix: distinguish Apps and Settings in the sidebar footer

Keep the horizontal footer layout with visible labels, matching button heights, a subtle divider, and an Apps dropdown indicator. Use a layout icon for Manage Apps so it is distinct from global Settings.

Release note: Make the bottom-left Apps and Settings entries easier to distinguish with labeled buttons and separate visual boundaries.
This commit is contained in:
mesalogo
2026-09-28 18:08:52 +08:00
parent c11f204b17
commit 8650907b75
14 changed files with 71 additions and 45 deletions
+21 -18
View File
@@ -6204,7 +6204,7 @@ describe("App", () => {
name: "选择知识库,本次已启用 1 个",
}),
).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '应用中心' }));
fireEvent.click(screen.getByRole('button', { name: '应用' }));
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }));
fireEvent.change(screen.getByRole('searchbox'), { target: { value: '知识库' } });
expect(within(screen.getByRole('dialog')).queryByRole('switch')).not.toBeInTheDocument();
@@ -13904,7 +13904,7 @@ describe("App", () => {
expect(screen.queryByRole("button", { name: "应用设置" })).not.toBeInTheDocument();
expect(api.updates.updateSettings).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("button", { name: "关闭设置" }));
fireEvent.click(screen.getByRole("button", { name: "应用中心" }));
fireEvent.click(screen.getByRole("button", { name: "应用" }));
fireEvent.click(screen.getByRole("menuitem", { name: "管理应用" }));
fireEvent.click(screen.getByRole("button", { name: "魔法笔记 应用设置" }));
const toggle = await screen.findByRole("switch", {
@@ -13942,7 +13942,7 @@ describe("App", () => {
await updates.updateSettings({ magicNotesEnabled: true })
render(<App />)
await screen.findByRole('button', { name: '魔法笔记' })
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
const move = screen.getByRole('button', { name: '下移 知识库' })
await waitFor(() => expect(move).toBeEnabled())
@@ -13962,7 +13962,7 @@ describe("App", () => {
expect(within(screen.getByRole('navigation', { name: '主导航' })).getAllByRole('button').map(button => button.textContent)).toEqual([
'对话', '监督者', '知识库', '魔法笔记', '运行记录',
])
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
expect(screen.getAllByRole('menuitem').map(item => item.textContent)).toEqual(['监督者', '本机推理监控', '知识库', '魔法笔记', '管理应用'])
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
expect(within(screen.getByRole('dialog', { name: '应用中心' })).getAllByRole('article').map(card => card.querySelector('strong')?.textContent)).toEqual(['监督者', '本机推理监控', '知识库', '魔法笔记'])
@@ -13982,7 +13982,7 @@ describe("App", () => {
},
}
vi.mocked(updates.getSettings).mockResolvedValue({ ...external, applicationNavigation: { ...external.applicationNavigation, order: [...external.applicationNavigation.order] } })
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
fireEvent.click(screen.getByRole('button', { name: '本机推理监控 应用设置' }))
const pin = screen.getByRole('switch', { name: '常驻左侧菜单' })
@@ -14014,7 +14014,7 @@ describe("App", () => {
await screen.findByRole('button', { name: '监督者' })
const reply = deferred<ApplicationSettings>()
if (operation === 'read') vi.mocked(updates.getSettings).mockReturnValueOnce(reply.promise)
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
fireEvent.click(screen.getByRole('button', { name: '本机推理监控 应用设置' }))
const pin = screen.getByRole('switch', { name: '常驻左侧菜单' })
@@ -14181,7 +14181,7 @@ describe("App", () => {
fireEvent.click(screen.getByRole('button', { name: /^笔记搜索/ }))
fireEvent.click(await screen.findByRole('button', { name: '新建笔记' }))
fireEvent.change(screen.getByLabelText('笔记标题'), { target: { value: 'Unsaved quick note' } })
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
const center = await screen.findByRole('dialog', { name: '应用中心' })
fireEvent.click(within(center).getByRole('button', { name: /魔法笔记.*设置|设置.*魔法笔记/ }))
@@ -14235,7 +14235,7 @@ describe("App", () => {
expect(await screen.findByRole('alertdialog')).toBeVisible()
expect(screen.queryByRole('button', { name: '关闭设置' })).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '继续编辑' }))
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '知识库' }))
expect(await screen.findByRole('alertdialog')).toBeVisible()
fireEvent.click(screen.getByRole('button', { name: '放弃草稿并切换' }))
@@ -14263,7 +14263,7 @@ describe("App", () => {
if (entry === 'sidebar') await api.updates!.updateSettings({ applicationNavigation: { ...defaultTestApplicationNavigation, pinned: { ...defaultTestApplicationNavigation.pinned, 'local-inference': true } } })
render(<App />)
await waitFor(() => expect(api.updates!.getSettings).toHaveBeenCalled())
const nav = entry === 'sidebar' ? await screen.findByRole('button', { name: '本机推理监控' }) : screen.getByRole('button', { name: '应用中心' })
const nav = entry === 'sidebar' ? await screen.findByRole('button', { name: '本机推理监控' }) : screen.getByRole('button', { name: '应用' })
if (entry !== 'sidebar') expect(within(screen.getByRole('navigation', { name: '主导航' })).getAllByRole('button').map(button => button.textContent)).toEqual(['对话', '知识库', '监督者', '运行记录'])
fireEvent.click(screen.getByRole('button', { name: '知识库' }))
const workspace = await screen.findByLabelText('知识工作区')
@@ -14271,7 +14271,7 @@ describe("App", () => {
nav.focus()
fireEvent.click(nav)
} else {
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
if (entry === 'menu') fireEvent.click(screen.getByRole('menuitem', { name: '本机推理监控' }))
else {
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
@@ -14290,7 +14290,7 @@ describe("App", () => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
expect(screen.getByLabelText('知识工作区')).toBe(workspace)
expect(workspace.closest('.app-shell')).toHaveProperty('inert', false)
expect(entry === 'sidebar' ? nav : screen.getByRole('button', { name: '应用中心' })).toHaveFocus()
expect(entry === 'sidebar' ? nav : screen.getByRole('button', { name: '应用' })).toHaveFocus()
})
it('saves Supervisor timeouts and concurrency through application settings and restores them on reopen', async () => {
@@ -14342,7 +14342,7 @@ describe("App", () => {
render(<App />)
expect(await screen.findByRole('button', { name: '监督者' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
fireEvent.click(screen.getByRole('button', { name: '监督者 应用设置' }))
const enable = screen.getByRole('switch', { name: '启用应用' })
@@ -14362,7 +14362,7 @@ describe("App", () => {
{ name: '监督者' },
),
).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
expect(screen.queryByRole('menuitem', { name: '监督者' })).not.toBeInTheDocument()
expect(api.heartbeats.remove).not.toHaveBeenCalled()
expect(await api.heartbeats.list()).toEqual([plan])
@@ -14377,7 +14377,7 @@ describe("App", () => {
})
render(<App />)
const inferenceNav = await screen.findByRole('button', { name: '本机推理监控' })
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
fireEvent.click(screen.getByRole('button', { name: '本机推理监控 应用设置' }))
const pin = screen.getByRole('switch', { name: '常驻左侧菜单' })
@@ -14385,11 +14385,14 @@ describe("App", () => {
fireEvent.click(pin)
await waitFor(() => expect(inferenceNav).not.toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: '关闭应用中心' }))
const trigger = screen.getByRole('button', { name: '应用中心' })
const trigger = screen.getByRole('button', { name: '应用', expanded: false })
expect(trigger).toHaveFocus()
expect(within(screen.getByRole('button', { name: '设置' })).getByText('设置')).toBeVisible()
fireEvent.click(trigger)
expect(trigger).toHaveAttribute('aria-expanded', 'true')
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('menuitem', { name: '本机推理监控' }))
expect(trigger).toHaveAttribute('aria-expanded', 'false')
const workspace = await screen.findByRole('region', { name: '本机推理服务' })
expect(within(screen.getByRole('navigation', { name: '主导航' })).queryByRole('button', { name: '本机推理监控' })).not.toBeInTheDocument()
await act(async () => changed(await api.updates!.updateSettings({ localInferenceEnabled: false })))
@@ -14403,14 +14406,14 @@ describe("App", () => {
expect((await api.updates!.getSettings()).applicationNavigation.pinned['local-inference']).toBe(false)
await act(async () => changed(await api.updates!.updateSettings({ localInferenceEnabled: false })))
fireEvent.click(screen.getByRole('button', { name: '关闭本机推理监控' }))
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
fireEvent.click(screen.getByRole('button', { name: '本机推理监控 应用设置' }))
await waitFor(() => expect(screen.getByRole('switch', { name: '启用应用' })).not.toBeDisabled())
fireEvent.click(screen.getByRole('switch', { name: '启用应用' }))
await waitFor(() => expect(screen.getByRole('switch', { name: '启用应用' })).toBeChecked())
fireEvent.click(screen.getByRole('button', { name: '关闭应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '本机推理监控' }))
expect(await screen.findByRole('region', { name: '本机推理服务' })).toBeVisible()
})
@@ -14418,7 +14421,7 @@ describe("App", () => {
it('keeps confirmed application preferences and locks edits until an unknown save is read back', async () => {
render(<App />)
await screen.findByRole('button', { name: '知识库' })
fireEvent.click(screen.getByRole('button', { name: '应用中心' }))
fireEvent.click(screen.getByRole('button', { name: '应用' }))
fireEvent.click(screen.getByRole('menuitem', { name: '管理应用' }))
const updates = api.updates!
fireEvent.click(screen.getByRole('button', { name: '本机推理监控 应用设置' }))
+6 -3
View File
@@ -9362,10 +9362,12 @@ function App(): React.JSX.Element {
}}
>
<Grid2X2 size={17} aria-hidden="true" />
<span>{t("applications.title")}</span>
<span>{t("applications.menuLabel")}</span>
<ChevronDown size={16} aria-hidden="true" />
</button>
<div className="sidebar-footer__divider" aria-hidden="true" />
<button
className="icon-button"
className="nav-item"
aria-label={t('navigation.settings')}
title={t('navigation.settings')}
type="button"
@@ -9375,7 +9377,8 @@ function App(): React.JSX.Element {
}
onPointerEnter={() => preloadWorkspaceRouteOnIntent("settings")}
>
<Settings size={16} />
<Settings size={16} aria-hidden="true" />
<span>{t('navigation.settings')}</span>
</button>
</div>
</aside>
+2 -2
View File
@@ -1,5 +1,5 @@
import { useMemo, type RefObject } from 'react'
import { Settings } from 'lucide-react'
import { LayoutGrid } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { defaultApplicationNavigation, type ApplicationSettings, type BuiltInApplicationId } from '../../shared/application-settings-contracts'
import { applicationDefinitions, isApplicationEnabled } from './ApplicationCenter'
@@ -36,7 +36,7 @@ export function ApplicationMenu({ anchorRef, settings, pending, error, onClose,
})}
<div role="separator" />
<button type="button" role="menuitem" tabIndex={-1} onClick={() => { onClose(); anchorRef.current?.focus(); onManage() }}>
<Settings size={16} aria-hidden="true" /><span>{t('applications.manage')}</span>
<LayoutGrid size={16} aria-hidden="true" /><span>{t('applications.manage')}</span>
</button>
</AnchoredMenu>
}
+13 -1
View File
@@ -205,5 +205,17 @@
gap: var(--space-2);
}
.sidebar-footer--applications > .nav-item {
flex: 1;
flex: 1 0 auto;
gap: var(--space-2);
padding-inline: var(--space-2);
justify-content: center;
white-space: nowrap;
}
.sidebar-footer--applications > .nav-item > span {
flex: 0 0 auto;
}
.sidebar-footer__divider {
flex: 0 0 1px;
height: var(--space-4);
background: var(--border-subtle);
}
@@ -9,6 +9,7 @@ export const app = {
priority: { low: 'Low priority', medium: 'Medium priority', high: 'High priority' }
},
applications: {
menuLabel: 'Apps',
alwaysShown: 'Always shown', optional: 'Optional',
manage: 'Manage Apps',
title: 'Application Center', description: 'Open and reorder built-in applications, and manage optional apps and pins. Knowledge is always shown; Supervisor can be enabled or disabled.',
@@ -6,6 +6,7 @@ export const app = {
priority: { low: '低优先级', medium: '中优先级', high: '高优先级' }
},
applications: {
menuLabel: '应用',
alwaysShown: '始终显示', optional: '可选',
manage: '管理应用',
title: '应用中心', description: '打开和排列内置应用,管理可选应用的启用与常驻。知识库始终显示,监督者可启用或关闭。',