From e4f7b8d9932ceaf0b4d4c23e2e644d3bc60b9d63 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Wed, 30 Sep 2026 23:33:25 +0800 Subject: [PATCH] fix: group settings navigation and align switches to the right --- UI-DESIGN.md | 7 +- .../CapabilitiesAndToolsSettingsSection.tsx | 1 - src/renderer/src/SettingsPanel.test.tsx | 21 ++-- src/renderer/src/SettingsPanel.tsx | 55 +++++----- .../src/i18n/locales/en-US/settings.ts | 10 +- .../src/i18n/locales/zh-CN/settings.ts | 10 +- src/renderer/src/settings-categories.ts | 24 +++- src/renderer/src/styles.css | 103 ++++++++++-------- .../workbar-settings-layout-driver.mjs | 56 +++++++++- 9 files changed, 191 insertions(+), 96 deletions(-) diff --git a/UI-DESIGN.md b/UI-DESIGN.md index 4ecdb6b9..19beabec 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -488,6 +488,7 @@ Switch 用于在两个持久状态之间立即切换,例如启用能力、开 - Checkbox 保留原生 Checkbox 语义和方形勾选视觉,不得添加 `role="switch"`。多项分配、列表选择、确认声明和“保存时清除密钥”等一次性选择均属于 Checkbox。 - 不创建页面专属 Switch 样式。需要紧凑布局时仍复用同一轨道、滑块、焦点环、禁用状态和动效,只调整共享组件支持的布局变体。 - Switch 支持 Tab 聚焦和 Space 切换,键盘焦点至少显示 `2px` 高对比焦点环。可见标签应描述被控制的能力,不能只显示“开 / 关”。 +- 共享开关行统一为标签在左、Switch 靠右,说明放在标签下方;不因 JSX 中输入控件与文字的先后顺序改变视觉位置。窄窗口允许文字换行,开关保持固定尺寸。Checkbox 和 Radio 保留各自的选择布局。 - 异步切换期间禁用重复操作并保留原状态。失败时恢复或保留最后确认状态,通过应用通知或就地可恢复错误说明原因。 - 涉及联网、上传、电脑控制或其他外部影响的 Switch,附近必须持续说明数据去向、权限范围或风险,不能只靠设置名称表达影响。 - 自动化测试应按 `switch` 角色查询二元开关,按 `checkbox` 角色查询多选或确认项,防止视觉迁移后语义回退。 @@ -825,10 +826,12 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证 - 设置中心以整窗遮罩上的大 Modal 打开,保留底层工作区、会话草稿和助手工作栏,不切换一级页面。固定标题区和右上角关闭按钮,左侧分类与右侧内容各自滚动。关闭后返回原工作区并恢复入口焦点。 - 新建项目与项目设置弹窗固定标题、关闭按钮和底部操作区,仅表单内容滚动;滚动条不进入标题区,关闭按钮保留至少 40 × 40px 的点击区域。 - 项目弹窗通过 Portal 挂载到 `document.body`,避免侧栏内的弹窗与主窗口标题栏拖动区域重叠时,Electron 截获关闭按钮的鼠标输入。 -- Modal 最大尺寸为 `1200px × 900px`,常规外边距为 `24px`;窗口宽度不超过 `1020px` 或高度不超过 `720px` 时接近全屏,保留 `16px` 外边距。左侧分类宽度依次为 `220px`、`180px`(宽度不超过 `1020px`)、`120px`(不超过 `720px`);极窄窗口隐藏分类辅助说明,仍保留左右布局。内容区在可用空间内伸缩,最大宽度为 `960px`。 +- Modal 最大尺寸为 `1200px × 900px`,常规外边距为 `24px`;窗口宽度不超过 `1020px` 或高度不超过 `720px` 时接近全屏,保留 `16px` 外边距。左侧分类宽度依次为 `220px`、`180px`(宽度不超过 `1020px`)、`120px`(不超过 `720px`);窄窗口仍保留左右布局,长分类名称可换行。内容区在可用空间内伸缩,最大宽度为 `960px`。 - 设置 Modal 通过 Portal 挂载到 `document.body`,复用共享背景隔离与焦点恢复机制。打开后聚焦关闭按钮,Tab 在弹窗内循环;Escape 先交由嵌套弹窗处理,设置自身的 Escape 与关闭按钮共用未保存确认,确认出现后再按 Escape 返回编辑。点击遮罩不关闭;保存、测试和清理数据期间禁止关闭。每次打开重新挂载设置,模型和消息通道等直达入口使用本次指定分类,普通入口默认打开 Agent Runtime。 - 设置中心不显示全局操作页脚,避免重复关闭入口和没有功能意义的整宽分隔线。 -- 所有分类使用共享的 `SettingsCategoryHeader` 呈现分类标题、说明、错误与操作,不得在内容卡片内复制分类标题或创建页面专属操作栏。左侧分类名称与说明来自同一份分类定义,新增分类时不得分别维护导航和内容标题。 +- 所有分类使用共享的 `SettingsCategoryHeader` 呈现分类标题、说明、错误与操作,不得在内容卡片内复制分类标题或创建页面专属操作栏。左侧分类名称与右侧标题来自同一份分类定义,新增分类时不得分别维护导航和内容标题。 +- 左侧按“常规 / 模型与执行 / 功能与扩展 / 系统”分组,顺序与键盘导航一致。分类按钮最小高度 `40px`,只显示名称;分类说明集中在右侧页头,外层标题不再重复功能简介。组名使用辅助字号,普通项使用正文和中等字重,选中项使用强调底色与加粗文字。 +- 设置区块使用共享圆角、间距和表面令牌;标题为 `--font-section-title`,持久说明至少为 `--font-caption`。嵌套设置区块以顶部细分隔线和留白组织,不叠加卡片背景与四周边框。“能力与工具”的上级页签使用默认 `PageTabs` 外观,与 MCP 内部的分段页签区分。 - 当前分类存在“保存”或“测试”等未提交配置操作时,统一放在分类页头右侧;主保存操作在最右侧,测试等次操作排列在其左侧。 - 自动生效、仅执行即时命令或自行管理编辑流程的分类不显示全局保存操作。窄窗口下操作区可以换行,但保存入口必须保持清晰可见。 - 智能心跳的单条配置不在设置中心重复管理。设置中心如需呈现平台级说明,只提供 diff --git a/src/renderer/src/CapabilitiesAndToolsSettingsSection.tsx b/src/renderer/src/CapabilitiesAndToolsSettingsSection.tsx index 9e95b4e8..a6619dc4 100644 --- a/src/renderer/src/CapabilitiesAndToolsSettingsSection.tsx +++ b/src/renderer/src/CapabilitiesAndToolsSettingsSection.tsx @@ -58,7 +58,6 @@ export function CapabilitiesAndToolsSettingsSection({ } ]} value={activeTab} - variant="segmented" /> } diff --git a/src/renderer/src/SettingsPanel.test.tsx b/src/renderer/src/SettingsPanel.test.tsx index 21446c96..93350f53 100644 --- a/src/renderer/src/SettingsPanel.test.tsx +++ b/src/renderer/src/SettingsPanel.test.tsx @@ -1163,7 +1163,7 @@ describe('SettingsPanel runtime files', () => { ).toBeInTheDocument() expect( screen.getByRole('tab', { name: '上下文控制' }) - ).toHaveTextContent('直连模型的历史压缩与原文保留') + ).toHaveTextContent(/^上下文控制$/) const enabled = screen.getByRole('switch', { name: '自动压缩较早的对话' }) @@ -1796,15 +1796,22 @@ describe('SettingsPanel runtime files', () => { expect(content.parentElement).toBe(navigation.parentElement) expect(content).toHaveClass('settings-panel__content') expect( - screen.getByText('管理模型、Runtime、平台能力、消息通道与本地数据。') - ).toBeInTheDocument() + screen.queryByText('管理模型、Runtime、平台能力、消息通道与本地数据。') + ).not.toBeInTheDocument() const runtimeTab = screen.getByRole('tab', { name: 'Agent Runtime' }) - expect(runtimeTab).toHaveTextContent('Agent Runtime') - expect(runtimeTab).toHaveTextContent( - '配置 Agent Runtime、默认工作区与原生能力' - ) + expect(runtimeTab).toHaveTextContent(/^Agent Runtime$/) + expect( + Array.from(navigation.querySelectorAll('.settings-tabs__group-label'), node => node.textContent) + ).toEqual(['常规', '模型与执行', '功能与扩展', '系统']) + expect(within(navigation).getAllByRole('tab').map(tab => tab.id)).toEqual([ + 'settings-tab-appearance', 'settings-tab-platform-features', + 'settings-tab-model', 'settings-tab-context-control', 'settings-tab-runtime', + 'settings-tab-ssh-hosts', + 'settings-tab-document-parsing', 'settings-tab-channels', 'settings-tab-roles', + 'settings-tab-capabilities', 'settings-tab-security', 'settings-tab-about' + ]) }) it('omits the redundant close-only footer on passive settings pages', () => { diff --git a/src/renderer/src/SettingsPanel.tsx b/src/renderer/src/SettingsPanel.tsx index ee1e930d..0a4bc1e4 100644 --- a/src/renderer/src/SettingsPanel.tsx +++ b/src/renderer/src/SettingsPanel.tsx @@ -10,6 +10,7 @@ import { X } from 'lucide-react' import { + Fragment, useCallback, useEffect, useLayoutEffect, @@ -2124,7 +2125,6 @@ export function SettingsPanel({