From 700dc053aca718ee270a42809a0922327f2692c6 Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 11:06:23 +0800 Subject: [PATCH 01/19] style(macos): align settings panels with IntelliJ UI --- docs/development/platform-parity-matrix.csv | 2 +- docs/development/platform-parity-matrix.md | 2 +- macos/Sources/Lithe/Theme/LitheTheme.swift | 56 ++- .../Views/App/GitExecutionSettingsView.swift | 46 +- .../Views/App/GitFetchSettingsView.swift | 45 +- .../Views/App/GitIdentitySettingsView.swift | 21 +- .../App/KeyboardShortcutSettingsView.swift | 37 +- .../App/ProjectRuntimeSettingsView.swift | 42 +- .../App/RunConfigurationSettingsView.swift | 7 +- .../Lithe/Views/App/SettingsView.swift | 149 +++--- .../Components/LitheSettingsControls.swift | 468 ++++++++++++++---- .../Views/Language/LSPControlCenterView.swift | 16 +- .../SettingsSelectPopupGeometryTests.swift | 29 ++ shared/platform-feature-matrix.json | 6 +- 14 files changed, 629 insertions(+), 297 deletions(-) create mode 100644 macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv index 1c6c29e25..8222abe79 100644 --- a/docs/development/platform-parity-matrix.csv +++ b/docs/development/platform-parity-matrix.csv @@ -69,7 +69,7 @@ outline-symbols,编辑器,代码结构,当前文件 Outline 与符号树,部分 references-pane,编辑器,代码结构,引用结果面板与引用跳转,已实现,待验证,已实现,待验证,Language Tooling,从符号发起引用查询,验证结果分组、文件定位、关闭和重新查询。,,macos/Sources/Lithe/Views/Language/JavaReferencesView.swift,windows/tauri/src/features/references diagnostics-workspace,可观测性,诊断与日志,工作区诊断与问题面板,已实现,待验证,已实现,待验证,Diagnostics,制造多个来源的错误和警告,验证筛选、排序、定位和清除。,,macos/Sources/Lithe/Application/Features/DiagnosticsFeatureModel.swift; macos/Sources/Lithe/Views/Language/JavaProblemsView.swift,windows/tauri/src/features/diagnostics; windows/tauri/src/features/logging; windows/tauri/src/ui/dialog.tsx diagnostics-export,可观测性,诊断与日志,诊断包导出与脱敏,已实现,待验证,已实现,待验证,Diagnostics,导出诊断包,确认敏感路径、凭据和大文件被正确脱敏或限制;检查预览说明和文件列表在确认弹窗内完整显示、长内容正常换行且不溢出。,,macos/Sources/Lithe/Views/Diagnostics/DiagnosticsExportSheet.swift; macos/Sources/Lithe/Services/Diagnostics,windows/tauri/src/features/diagnostics -application-settings,设置,应用配置,应用、项目与运行设置,已实现,待验证,已实现,待验证,Settings,修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 保持设置窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift,windows/tauri/src/features/settings +application-settings,设置,应用配置,应用、项目与运行设置,已实现,待验证,已实现,待验证,Settings,修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift; macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift,windows/tauri/src/features/settings keymap-customization,设置,应用配置,快捷键查看与自定义,已实现,待验证,已实现,待验证,Settings,从设置 → 快捷键打开编辑器,按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。,Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。,macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift; macos/Sources/Lithe/Models/Keymap,windows/tauri/src/features/keymaps; windows/tauri/src/features/settings/components/macos-settings-panels.tsx; windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx; windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx plugin-extension-management,扩展,插件与主题,插件、语言服务与主题管理,已实现,待验证,已实现,待验证,Extensions,启用、禁用和恢复插件/主题,确认启动加载、失败隔离和设置持久化。,,macos/Sources/Lithe/Views/App/PluginManagementView.swift; macos/Sources/Lithe/Platform/MacOS/Plugins,windows/tauri/src/extensions; windows/tauri/src/features/settings onboarding-welcome,启动与窗口,首次启动,欢迎页、项目入口与首次启动引导,已实现,待验证,已实现,待验证,Onboarding,清空首次启动状态,验证打开项目、克隆项目、最近项目和跳过引导。,,macos/Sources/Lithe/Views/App/WelcomeView.swift; macos/Sources/Lithe/Views/Workspace,windows/tauri/src/features/onboarding; windows/tauri/src/features/bootstrap diff --git a/docs/development/platform-parity-matrix.md b/docs/development/platform-parity-matrix.md index e55e4495b..b41a29c81 100644 --- a/docs/development/platform-parity-matrix.md +++ b/docs/development/platform-parity-matrix.md @@ -204,7 +204,7 @@ | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | -| 应用配置 | **应用、项目与运行设置**
application-settings | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings` | Settings | 修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 保持设置窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。 | | +| 应用配置 | **应用、项目与运行设置**
application-settings | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift`、`macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift`、`macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings` | Settings | 修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。 | | | 应用配置 | **快捷键查看与自定义**
keymap-customization | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift`、`macos/Sources/Lithe/Models/Keymap` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/keymaps`、`windows/tauri/src/features/settings/components/macos-settings-panels.tsx`、`windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx`、`windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx` | Settings | 从设置 → 快捷键打开编辑器,按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。 | Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。 | diff --git a/macos/Sources/Lithe/Theme/LitheTheme.swift b/macos/Sources/Lithe/Theme/LitheTheme.swift index 02be82623..c15b04ff1 100644 --- a/macos/Sources/Lithe/Theme/LitheTheme.swift +++ b/macos/Sources/Lithe/Theme/LitheTheme.swift @@ -276,6 +276,7 @@ enum LitheTheme { // MARK: - 背景层次 static var window: Color { adaptive(\.window) } static var titlebar: Color { adaptive(\.titlebar) } + // IntelliJ Community Islands theme tokens: platform/platform-resources/src/themes/islands/ManyIslands{Dark,Light}.theme.json. static var settingsSurface: Color { Color(nsColor: NSColor(name: nil) { appearance in settingsSurfaceNSColor(for: appearance) @@ -284,23 +285,60 @@ enum LitheTheme { static func settingsSurfaceNSColor(for appearance: NSAppearance) -> NSColor { let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua return isDark - ? NSColor(srgbRed: 0.157, green: 0.161, blue: 0.173, alpha: 1) - : NSColor(srgbRed: 0.910, green: 0.922, blue: 0.937, alpha: 1) - } - static var settingsPrimaryAction: Color { accent } - static let settingsSelection = Color( - red: 43.0 / 255.0, - green: 66.0 / 255.0, - blue: 113.0 / 255.0 + ? NSColor(srgbRed: 25.0 / 255.0, green: 26.0 / 255.0, blue: 28.0 / 255.0, alpha: 1) + : NSColor(srgbRed: 247.0 / 255.0, green: 248.0 / 255.0, blue: 249.0 / 255.0, alpha: 1) + } + static let settingsControlAccent = Color( + red: 56.0 / 255.0, + green: 113.0 / 255.0, + blue: 225.0 / 255.0 ) + static var settingsPrimaryAction: Color { settingsControlAccent } + static var settingsSelection: Color { + Color(nsColor: NSColor(name: nil) { appearance in + let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua + return isDark + ? NSColor(srgbRed: 42.0 / 255.0, green: 67.0 / 255.0, blue: 113.0 / 255.0, alpha: 1) + : NSColor(srgbRed: 208.0 / 255.0, green: 223.0 / 255.0, blue: 254.0 / 255.0, alpha: 1) + }) + } + static var settingsSelectionText: Color { + Color(nsColor: NSColor(name: nil) { appearance in + let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua + return isDark ? .white : .black + }) + } static var settingsControlBackground: Color { Color(nsColor: NSColor(name: nil) { appearance in let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua return isDark - ? NSColor(srgbRed: 43.0 / 255.0, green: 45.0 / 255.0, blue: 48.0 / 255.0, alpha: 1) + ? NSColor(srgbRed: 38.0 / 255.0, green: 40.0 / 255.0, blue: 44.0 / 255.0, alpha: 1) : NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) }) } + static var settingsTextFieldBackground: Color { + Color(nsColor: NSColor(name: nil) { appearance in + let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua + return isDark ? settingsSurfaceNSColor(for: appearance) : .white + }) + } + static var settingsControlBorder: Color { + Color(nsColor: NSColor(name: nil) { appearance in + let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua + return isDark + ? NSColor(srgbRed: 64.0 / 255.0, green: 67.0 / 255.0, blue: 74.0 / 255.0, alpha: 1) + : NSColor(srgbRed: 209.0 / 255.0, green: 211.0 / 255.0, blue: 217.0 / 255.0, alpha: 1) + }) + } + static var settingsPopupBackground: Color { settingsControlBackground } + static var settingsPopupBorder: Color { + Color(nsColor: NSColor(name: nil) { appearance in + let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua + return isDark + ? NSColor(srgbRed: 76.0 / 255.0, green: 79.0 / 255.0, blue: 86.0 / 255.0, alpha: 1) + : NSColor(srgbRed: 233.0 / 255.0, green: 234.0 / 255.0, blue: 238.0 / 255.0, alpha: 1) + }) + } static var toolHeader: Color { adaptive(\.toolHeader) } static var toolHeaderInactive: Color { adaptive(\.toolHeaderInactive) } static var sidebar: Color { adaptive(\.sidebar) } diff --git a/macos/Sources/Lithe/Views/App/GitExecutionSettingsView.swift b/macos/Sources/Lithe/Views/App/GitExecutionSettingsView.swift index eb0ad34dd..b726bb9e2 100644 --- a/macos/Sources/Lithe/Views/App/GitExecutionSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/GitExecutionSettingsView.swift @@ -10,12 +10,12 @@ struct GitExecutionSettingsView: View { var body: some View { GitSettingsCard { - GitSettingsHeader(icon: "arrow.triangle.branch", title: "Git execution", subtitle: "Choose how Lithe locates Git and handles credentials.") + GitSettingsHeader(title: "Git execution", subtitle: "Choose how Lithe locates Git and handles credentials.") GitSettingsRow("Git executable") { HStack(spacing: 8) { TextField("Use Git from PATH", text: $executable) - .textFieldStyle(.roundedBorder) + .litheSettingsTextField() .frame(maxWidth: .infinity) Button("Save") { settings.gitExecutable = executable.trimmingCharacters(in: .whitespacesAndNewlines) } .buttonStyle(LithePrimaryButtonStyle(backgroundColor: LitheTheme.settingsPrimaryAction, restingOpacity: 1)) @@ -27,7 +27,7 @@ struct GitExecutionSettingsView: View { VStack(alignment: .leading, spacing: 7) { GitSettingsRow("Credentials") { - Toggle("Use credential helper", isOn: $settings.gitUseCredentialHelper) + LitheSettingsCheckbox(isOn: $settings.gitUseCredentialHelper, title: "Use credential helper") } Text("Git can request credentials in Lithe. Passwords are not saved by Lithe; the selected Git helper controls credential storage.") .font(LitheTheme.smallFont).foregroundStyle(LitheTheme.secondaryText) @@ -55,22 +55,25 @@ struct GitSettingsCard: View { @ViewBuilder let content: Content var body: some View { - VStack(alignment: .leading, spacing: 14, content: { content }) - .padding(.vertical, 14) + VStack(alignment: .leading, spacing: 10, content: { content }) + .padding(.top, 16) .frame(maxWidth: .infinity, alignment: .leading) - .overlay(alignment: .bottom) { Rectangle().fill(LitheTheme.divider).frame(height: 1) } } } struct GitSettingsHeader: View { - let icon: String let title: LocalizedStringKey let subtitle: LocalizedStringKey var body: some View { - VStack(alignment: .leading, spacing: 3) { - Label(title, systemImage: icon) - .font(.system(size: 14, weight: .semibold)) + VStack(alignment: .leading, spacing: 5) { + HStack(spacing: 8) { + Text(title) + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(LitheTheme.secondaryText) + .fixedSize() + Rectangle().fill(LitheTheme.divider).frame(height: 1) + } Text(subtitle).font(LitheTheme.smallFont).foregroundStyle(LitheTheme.secondaryText) } } @@ -112,12 +115,12 @@ private struct GitExecutionConfigurationPane: View { subtitle: "Choose whether this setting belongs to the open repository or to Git everywhere." ) GitSettingsRow("Configuration scope") { - Picker("Configuration scope", selection: $scope) { - Text("Current repository").tag("local") - Text("Global Git configuration").tag("global") - } - .labelsHidden() - .pickerStyle(.segmented) + LitheSettingsSegmentedControl( + selection: $scope, + options: ["local", "global"], + width: 360, + title: { $0 == "local" ? "Current repository" : "Global Git configuration" } + ) } GitBranchContextView(feature: feature, remoteURL: remoteURL) @@ -130,16 +133,7 @@ private struct GitExecutionConfigurationPane: View { title: "Git configuration", subtitle: "Choose a behavior on the right. Lithe shows the value Git is using and the value a reset would restore." ) - HStack(spacing: 10) { - Image(systemName: "slider.horizontal.3") - .foregroundStyle(LitheTheme.secondaryText) - Text("Show Git key names") - .font(.system(size: 11.5, weight: .medium)) - Toggle("Show Git key names", isOn: $showsTechnicalDetails) - .labelsHidden() - .toggleStyle(.switch) - } - .foregroundStyle(LitheTheme.secondaryText) + LitheSettingsCheckbox(isOn: $showsTechnicalDetails, title: "Show Git key names") configurationGroup("Update", subtitle: "How Pull and Update Project combine your local and remote commits.", icon: "arrow.triangle.2.circlepath", fields: snapshot.fields.filter(isUpdateField), entries: snapshot.entries, fetchOptions: snapshot.fetchOptions) configurationGroup("Fetch", subtitle: "What Fetch removes or downloads before you start working.", icon: "arrow.down.circle", fields: snapshot.fields.filter(isFetchField), entries: snapshot.entries, fetchOptions: snapshot.fetchOptions) configurationGroup("Push", subtitle: "Where Push goes when you do not choose a target explicitly.", icon: "arrow.up.circle", fields: snapshot.fields.filter(isPushField), entries: snapshot.entries, fetchOptions: snapshot.fetchOptions) diff --git a/macos/Sources/Lithe/Views/App/GitFetchSettingsView.swift b/macos/Sources/Lithe/Views/App/GitFetchSettingsView.swift index 5560fad46..6c7b986b0 100644 --- a/macos/Sources/Lithe/Views/App/GitFetchSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/GitFetchSettingsView.swift @@ -5,7 +5,7 @@ struct GitFetchSettingsView: View { @Binding var options: GitFetchOptions var body: some View { GitSettingsCard { - GitSettingsHeader(icon: "arrow.down.circle", title: "Fetch defaults", subtitle: "Set the defaults used by ordinary Fetch in every project.") + GitSettingsHeader(title: "Fetch defaults", subtitle: "Set the defaults used by ordinary Fetch in every project.") GitFetchPolicyControls(options: $options) Text("Credentials use your existing Git helper and SSH configuration.") .font(LitheTheme.smallFont).foregroundStyle(LitheTheme.secondaryText) @@ -19,23 +19,40 @@ struct GitFetchPolicyControls: View { var body: some View { VStack(alignment: .leading, spacing: 9) { GitSettingsRow("Prune") { - Toggle("Prune stale remote-tracking references", isOn: $options.prune) + LitheSettingsCheckbox(isOn: $options.prune, title: "Prune stale remote-tracking references") } GitSettingsRow("Fetch submodules") { - Picker("Fetch submodules", selection: $options.submodules) { - Text("Use Git configuration").tag(GitFetchSubmodules.inherit) - Text("Do not fetch submodules").tag(GitFetchSubmodules.no) - Text("Fetch submodules on demand").tag(GitFetchSubmodules.onDemand) - Text("Fetch all submodules").tag(GitFetchSubmodules.yes) - }.labelsHidden() + LitheSettingsSelect( + selection: $options.submodules, + options: GitFetchSubmodules.allCases, + width: 240, + accessibilityLabel: "Fetch submodules", + title: { + switch $0 { + case .inherit: "Use Git configuration" + case .no: "Do not fetch submodules" + case .onDemand: "Fetch submodules on demand" + case .yes: "Fetch all submodules" + } + } + ) } GitSettingsRow("Fetch tags") { - Picker("Fetch tags", selection: $options.tags) { - Text("Use Git configuration").tag(GitFetchTags.inherit) - Text("Fetch all tags").tag(GitFetchTags.all) - Text("Do not fetch tags").tag(GitFetchTags.none) - Text("Synchronize tags and remove local tags missing from the remote").tag(GitFetchTags.prune) - }.labelsHidden() + LitheSettingsSelect( + selection: $options.tags, + options: GitFetchTags.allCases, + width: 240, + accessibilityLabel: "Fetch tags", + title: { + switch $0 { + case .inherit: "Use Git configuration" + case .all: "Fetch all tags" + case .none: "Do not fetch tags" + case .prune: "Synchronize tags and remove local tags missing from the remote" + } + }, + expandsToFitOptions: true + ) } } .onChange(of: options.tags) { tags in if tags == .prune { options.prune = true } } diff --git a/macos/Sources/Lithe/Views/App/GitIdentitySettingsView.swift b/macos/Sources/Lithe/Views/App/GitIdentitySettingsView.swift index 222939cc2..2d636fd57 100644 --- a/macos/Sources/Lithe/Views/App/GitIdentitySettingsView.swift +++ b/macos/Sources/Lithe/Views/App/GitIdentitySettingsView.swift @@ -26,13 +26,22 @@ private struct GitIdentitySettingsPane: View { var body: some View { VStack(alignment: .leading, spacing: 18) { - Text("Commit identity").font(.system(size: 15, weight: .semibold)) + HStack(spacing: 8) { + Text("Commit identity") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(LitheTheme.secondaryText) + .fixedSize() + Rectangle().fill(LitheTheme.divider).frame(height: 1) + } Text("Git records this name and email in new commits. These settings do not change existing commits.") .font(LitheTheme.smallFont).foregroundStyle(LitheTheme.secondaryText) - Picker("Configuration scope", selection: $scope) { - Text("Current repository").tag(GitIdentityScope.local) - Text("Global Git configuration").tag(GitIdentityScope.global) - }.pickerStyle(.segmented).disabled(editor.isBusy) + LitheSettingsSegmentedControl( + selection: $scope, + options: GitIdentityScope.allCases, + width: 360, + title: { $0 == .local ? "Current repository" : "Global Git configuration" } + ) + .disabled(editor.isBusy) Text(scope == .global ? LocalizedStringKey("Global identity applies to other repositories unless they override it.") : LocalizedStringKey("Repository identity overrides inherited global values. Clear an override to use inherited settings.")) @@ -69,7 +78,7 @@ private struct GitIdentitySettingsPane: View { VStack(alignment: .leading, spacing: 7) { Text(title).font(.system(size: 12, weight: .medium)) HStack { - TextField(title, text: draft).textFieldStyle(.roundedBorder) + TextField(title, text: draft).litheSettingsTextField() Button("Save") { save(field) } .disabled(draft.wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || draft.wrappedValue == configured) .lithePointer() diff --git a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift index 760738d9b..9b9ebacc5 100644 --- a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift @@ -38,8 +38,8 @@ struct KeyboardShortcutSettingsView: View { } } } - .padding(.horizontal, 24) - .padding(.bottom, 24) + .padding(.horizontal, 16) + .padding(.bottom, 20) } .litheScrollViewChrome(alwaysShowVertical: true, usesCompactScrollers: true) } @@ -47,15 +47,11 @@ struct KeyboardShortcutSettingsView: View { } private var header: some View { - VStack(alignment: .leading, spacing: 14) { + VStack(alignment: .leading, spacing: 12) { HStack(alignment: .firstTextBaseline) { - VStack(alignment: .leading, spacing: 5) { - Text("Keymap") - .font(.system(size: 20, weight: .semibold)) - Text("Customize shortcuts for Lithe actions. Changes apply immediately.") - .font(LitheTheme.smallFont) - .foregroundStyle(LitheTheme.secondaryText) - } + Text("Customize shortcuts for Lithe actions. Changes apply immediately.") + .font(LitheTheme.smallFont) + .foregroundStyle(LitheTheme.secondaryText) Spacer() Button("Restore All Defaults") { editingTarget = nil @@ -72,15 +68,19 @@ struct KeyboardShortcutSettingsView: View { } } .foregroundStyle(LitheTheme.primaryText) - .padding(24) + .padding(.horizontal, 16) + .padding(.bottom, 16) } private func commandSection(_ section: KeyboardShortcutCommandSection) -> some View { VStack(alignment: .leading, spacing: 8) { - Text(LocalizedStringKey(section.group.rawValue)) - .font(.system(size: 11.5, weight: .semibold)) - .foregroundStyle(LitheTheme.secondaryText) - .textCase(.uppercase) + HStack(spacing: 8) { + Text(LocalizedStringKey(section.group.rawValue)) + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(LitheTheme.secondaryText) + .fixedSize() + Rectangle().fill(LitheTheme.divider).frame(height: 1) + } VStack(spacing: 0) { ForEach(Array(section.commands.enumerated()), id: \.element.id) { index, command in commandRow(command) @@ -202,12 +202,7 @@ struct KeyboardShortcutSettingsView: View { } .padding(.horizontal, 7) .frame(height: 24) - .background(LitheTheme.settingsControlBackground) - .clipShape(RoundedRectangle(cornerRadius: 4)) - .overlay { - RoundedRectangle(cornerRadius: 4) - .stroke(LitheTheme.inputBorder, lineWidth: 1) - } + .litheSettingsControlChrome() } private var emptyState: some View { diff --git a/macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift b/macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift index c5aacec88..c2b74267e 100644 --- a/macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift @@ -13,7 +13,6 @@ struct ProjectRuntimeSettingsView: View { var body: some View { VStack(spacing: 0) { header - Rectangle().fill(LitheTheme.divider).frame(height: 1) if model.workspaceURL == nil { emptyWorkspace } else { @@ -43,9 +42,6 @@ struct ProjectRuntimeSettingsView: View { private var header: some View { HStack { - Text("Project · JDK & Maven") - .font(.system(size: 15, weight: .semibold)) - .foregroundStyle(LitheTheme.primaryText) Spacer() if feature.isDiscovering { ProgressView() @@ -72,7 +68,7 @@ struct ProjectRuntimeSettingsView: View { .disabled(feature.isDiscovering) } .padding(.horizontal, 16) - .frame(height: 42) + .frame(height: 40) .background(LitheTheme.settingsSurface) } @@ -85,7 +81,7 @@ struct ProjectRuntimeSettingsView: View { .font(LitheTheme.smallFont) .foregroundStyle(LitheTheme.secondaryText) } - .padding(28) + .padding(16) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } @@ -120,7 +116,7 @@ struct ProjectRuntimeSettingsView: View { if subproject.displaysPath { Text(subproject.relativePath) .font(.system(size: 10.5, design: .monospaced)) - .foregroundStyle(isSelected ? Color.white.opacity(0.8) : LitheTheme.tertiaryText) + .foregroundStyle(isSelected ? LitheTheme.settingsSelectionText.opacity(0.8) : LitheTheme.tertiaryText) .lineLimit(1) } } @@ -134,7 +130,7 @@ struct ProjectRuntimeSettingsView: View { .contentShape(Rectangle()) } .buttonStyle(LitheTreeRowButtonStyle()) - .foregroundStyle(isSelected ? Color.white : LitheTheme.primaryText) + .foregroundStyle(isSelected ? LitheTheme.settingsSelectionText : LitheTheme.primaryText) .padding(.leading, subproject.parentID == nil ? 0 : 12) } @@ -148,9 +144,9 @@ struct ProjectRuntimeSettingsView: View { VStack(alignment: .leading, spacing: 8) { if let subproject = selectedSubproject { Text(subproject.title) - .font(.system(size: 22, weight: .semibold)) + .font(.system(size: 13, weight: .semibold)) .foregroundStyle(LitheTheme.primaryText) - .padding(.bottom, 8) + .padding(.bottom, 4) if subproject.kind == .projectDefaults { projectDefaultsDetail } else if subproject.usesJava { @@ -160,8 +156,8 @@ struct ProjectRuntimeSettingsView: View { } } } - .padding(.horizontal, 28) - .padding(.vertical, 22) + .padding(.horizontal, 16) + .padding(.bottom, 20) .frame(maxWidth: .infinity, alignment: .leading) } .litheScrollViewChrome(alwaysShowVertical: true, usesCompactScrollers: true) @@ -440,26 +436,28 @@ struct ProjectRuntimeSettingsView: View { } private func group(_ title: String, @ViewBuilder content: () -> Content) -> some View { - VStack(alignment: .leading, spacing: 13) { - Text(LocalizedStringKey(title)) - .font(.system(size: 12, weight: .semibold)) - .foregroundStyle(LitheTheme.secondaryText) + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 8) { + Text(LocalizedStringKey(title)) + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(LitheTheme.secondaryText) + .fixedSize() + Rectangle().fill(LitheTheme.divider).frame(height: 1) + } content() } .font(.system(size: 12.5)) .foregroundStyle(LitheTheme.primaryText) - .padding(.vertical, 16) + .padding(.top, 16) .frame(maxWidth: .infinity, alignment: .leading) - .overlay(alignment: .bottom) { - Rectangle().fill(LitheTheme.divider).frame(height: 1) - } } private func row(_ title: String, @ViewBuilder content: () -> Content) -> some View { - HStack { + HStack(spacing: 8) { Text(LocalizedStringKey(title)) - Spacer() + .fixedSize(horizontal: true, vertical: false) content() + Spacer(minLength: 0) } .frame(minHeight: 28) } diff --git a/macos/Sources/Lithe/Views/App/RunConfigurationSettingsView.swift b/macos/Sources/Lithe/Views/App/RunConfigurationSettingsView.swift index 2b612a509..0cc370d76 100644 --- a/macos/Sources/Lithe/Views/App/RunConfigurationSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/RunConfigurationSettingsView.swift @@ -37,9 +37,9 @@ private struct RunConfigurationSettingsContent: View { .id(configuration.id) } else { ScrollView { - VStack(alignment: .leading, spacing: 14) { - Text("Run configurations").font(.title2) + VStack(alignment: .leading, spacing: 12) { Text("Select a service or task to configure its arguments, environment and project-environment overrides. Changes apply after Save.") + .font(LitheTheme.smallFont) .foregroundStyle(LitheTheme.secondaryText) if feature.isLoadingProject || isGenerating { ProgressView("Identifying project…") @@ -72,7 +72,8 @@ private struct RunConfigurationSettingsContent: View { .disabled(feature.configurationStatus != .ready || feature.isLoadingProject || isGenerating) } } - .padding(24) + .padding(.horizontal, 16) + .padding(.bottom, 20) } .confirmationDialog("Identify Again", isPresented: $confirmGeneration) { Button("Rescan") { diff --git a/macos/Sources/Lithe/Views/App/SettingsView.swift b/macos/Sources/Lithe/Views/App/SettingsView.swift index fe00f9ebe..0974431ad 100644 --- a/macos/Sources/Lithe/Views/App/SettingsView.swift +++ b/macos/Sources/Lithe/Views/App/SettingsView.swift @@ -163,7 +163,7 @@ struct SettingsView: View { .contentShape(Rectangle()) } .buttonStyle(LitheTreeRowButtonStyle()) - .foregroundStyle(isSelected ? Color.white : LitheTheme.primaryText) + .foregroundStyle(isSelected ? LitheTheme.settingsSelectionText : LitheTheme.primaryText) } private var filteredCategories: [SettingsCategory] { @@ -215,8 +215,48 @@ struct SettingsView: View { ) } - @ViewBuilder private var content: some View { + VStack(spacing: 0) { + HStack(spacing: 8) { + ForEach(Array(settingsBreadcrumb.enumerated()), id: \.offset) { index, title in + if index > 0 { + Image(systemName: "chevron.right") + .font(.system(size: 9, weight: .medium)) + .foregroundStyle(LitheTheme.secondaryText) + } + Text(LocalizedStringKey(title)) + .font(.system(size: 12.5, weight: .semibold)) + .foregroundStyle(LitheTheme.primaryText) + } + Spacer(minLength: 0) + } + .padding(.horizontal, 16) + .frame(height: 48) + + settingsContent + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + private var settingsBreadcrumb: [String] { + switch viewState.selection { + case .general: ["Appearance & Behavior", "General"] + case .editor: ["Editor"] + case .keymap: ["Keymap"] + case .project: ["Build, Execution, Deployment", "Project · JDK & Maven"] + case .run: ["Build, Execution, Deployment", "Run configurations"] + case .terminal: ["Tools", "Terminal"] + case .lsp: ["Languages & Frameworks", "LSP"] + case .ai: ["Tools", "AI & Commit"] + case .providers: ["Tools", "AI Providers"] + case .git: ["Version Control", "Git"] + case .updates: ["Appearance & Behavior", "Updates"] + case .diagnostics: ["Tools", "Diagnostics"] + } + } + + @ViewBuilder + private var settingsContent: some View { if filteredCategories.isEmpty { VStack(spacing: 10) { Image(systemName: "magnifyingglass") @@ -245,12 +285,7 @@ struct SettingsView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } else { ScrollView { - VStack(alignment: .leading, spacing: 8) { - Text(LocalizedStringKey(viewState.selection.title)) - .font(.system(size: 22, weight: .semibold)) - .foregroundStyle(LitheTheme.primaryText) - .padding(.bottom, 8) - + VStack(alignment: .leading, spacing: 12) { switch viewState.selection { case .general: generalSettings case .editor: editorSettings @@ -272,8 +307,8 @@ struct SettingsView: View { case .diagnostics: diagnosticsSettings } } - .padding(.horizontal, 28) - .padding(.vertical, 22) + .padding(.horizontal, 16) + .padding(.bottom, 20) .frame(maxWidth: .infinity, alignment: .leading) } .litheScrollViewChrome(alwaysShowVertical: true, usesCompactScrollers: true) @@ -287,7 +322,7 @@ struct SettingsView: View { LitheSettingsSelect( selection: $settings.colorTheme, options: AppColorTheme.allCases, - width: 180, + width: 190, accessibilityLabel: "Color theme", title: \AppColorTheme.title ) @@ -371,7 +406,7 @@ struct SettingsView: View { LitheSettingsSelect( selection: $settings.language, options: AppLanguage.allCases, - width: 180, + width: 190, accessibilityLabel: "Language", title: \AppLanguage.title ) @@ -387,7 +422,7 @@ struct SettingsView: View { LitheSettingsSelect( selection: $settings.projectOpenBehavior, options: ProjectOpenBehavior.allCases, - width: 180, + width: 190, accessibilityLabel: "Open projects in", title: \ProjectOpenBehavior.title ) @@ -444,11 +479,7 @@ struct SettingsView: View { .font(.system(size: 12, design: .monospaced)) .frame(height: 66) .padding(5) - .litheRoundedControlBackground(LitheTheme.inputBackground) - .overlay { - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .stroke(LitheTheme.inputBorder, lineWidth: 1) - } + .litheSettingsTextEditor() Text("File patterns") .font(.system(size: 11.5, weight: .medium)) @@ -456,11 +487,7 @@ struct SettingsView: View { .font(.system(size: 12, design: .monospaced)) .frame(height: 52) .padding(5) - .litheRoundedControlBackground(LitheTheme.inputBackground) - .overlay { - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .stroke(LitheTheme.inputBorder, lineWidth: 1) - } + .litheSettingsTextEditor() HStack(spacing: 8) { Spacer() @@ -505,12 +532,7 @@ struct SettingsView: View { } .padding(.horizontal, 12) .frame(maxWidth: .infinity, minHeight: 46, maxHeight: 46) - .background(LitheTheme.inputBackground) - .clipShape(RoundedRectangle(cornerRadius: 6)) - .overlay { - RoundedRectangle(cornerRadius: 6) - .stroke(LitheTheme.inputBorder, lineWidth: 1) - } + .litheSettingsControlChrome(background: LitheTheme.settingsTextFieldBackground) HStack(spacing: 6) { Text("Default directory") @@ -962,11 +984,7 @@ struct SettingsView: View { .font(.system(size: 12, design: .monospaced)) .frame(height: 92) .padding(5) - .litheRoundedControlBackground(LitheTheme.inputBackground) - .overlay { - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .stroke(LitheTheme.inputBorder, lineWidth: 1) - } + .litheSettingsTextEditor() } Text("Low effort and a small output limit are recommended for fast commit-message generation.") @@ -975,13 +993,15 @@ struct SettingsView: View { } group("Pull request description generation") { - Picker("Description format", selection: $settings.commitMessageAI.pullRequestFormat) { - ForEach(PullRequestDescriptionFormat.allCases) { format in - Text(LocalizedStringKey(format.title)).tag(format) - } + row("Description format") { + LitheSettingsSelect( + selection: $settings.commitMessageAI.pullRequestFormat, + options: PullRequestDescriptionFormat.allCases, + width: 220, + accessibilityLabel: "Description format", + title: \PullRequestDescriptionFormat.title + ) } - .frame(maxWidth: 260, alignment: .leading) - .lithePointer() if settings.commitMessageAI.pullRequestFormat == .custom { HStack { @@ -1001,12 +1021,7 @@ struct SettingsView: View { .font(.system(size: 12, design: .monospaced)) .frame(height: 150) .padding(5) - .background(LitheTheme.inputBackground) - .clipShape(RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)) - .overlay { - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .stroke(LitheTheme.inputBorder, lineWidth: 1) - } + .litheSettingsTextEditor() Text("Supported placeholders: {summary}, {changes}, {testing}, {risks}.") .font(LitheTheme.smallFont) @@ -1063,15 +1078,10 @@ struct SettingsView: View { } .padding(.horizontal, 10) .frame(maxWidth: .infinity, minHeight: 48, alignment: .leading) - .background(viewState.isFormatPickerPresented ? LitheTheme.inputBackground.opacity(0.9) : LitheTheme.inputBackground) - .overlay { - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .stroke( - viewState.isFormatPickerPresented ? LitheTheme.inputFocusBorder : LitheTheme.inputBorder, - lineWidth: 1 - ) - } - .clipShape(RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)) + .litheSettingsControlChrome( + background: LitheTheme.settingsTextFieldBackground, + border: viewState.isFormatPickerPresented ? LitheTheme.settingsControlAccent : LitheTheme.settingsControlBorder + ) } .buttonStyle(.plain) .lithePointer() @@ -1208,12 +1218,7 @@ struct SettingsView: View { .padding(.horizontal, 9) .padding(.vertical, 8) .frame(maxWidth: .infinity, alignment: .leading) - .background(LitheTheme.inputBackground) - .clipShape(RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)) - .overlay { - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .stroke(LitheTheme.inputBorder, lineWidth: 1) - } + .litheSettingsControlChrome(background: LitheTheme.settingsTextFieldBackground) .id(settings.commitMessageAI.format) .transition(.opacity.combined(with: .move(edge: .top))) .animation(formatPickerAnimation, value: settings.commitMessageAI.format) @@ -1393,26 +1398,28 @@ struct SettingsView: View { } private func group(_ title: String, @ViewBuilder content: () -> Content) -> some View { - VStack(alignment: .leading, spacing: 13) { - Text(LocalizedStringKey(title)) - .font(.system(size: 12, weight: .semibold)) - .foregroundStyle(LitheTheme.secondaryText) + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 8) { + Text(LocalizedStringKey(title)) + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(LitheTheme.secondaryText) + .fixedSize() + Rectangle().fill(LitheTheme.divider).frame(height: 1) + } content() } .font(.system(size: 12.5)) .foregroundStyle(LitheTheme.primaryText) - .padding(.vertical, 16) + .padding(.top, 16) .frame(maxWidth: .infinity, alignment: .leading) - .overlay(alignment: .bottom) { - Rectangle().fill(LitheTheme.divider).frame(height: 1) - } } private func row(_ title: String, @ViewBuilder content: () -> Content) -> some View { - HStack { + HStack(spacing: 8) { Text(LocalizedStringKey(title)) - Spacer() + .fixedSize(horizontal: true, vertical: false) content() + Spacer(minLength: 0) } .frame(minHeight: 28) } diff --git a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift index 3e573460d..e4ea84dda 100644 --- a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift +++ b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift @@ -2,12 +2,33 @@ import AppKit import SwiftUI private enum SettingsSelectMetrics { + static let controlHeight: CGFloat = 28 + static let controlCornerRadius: CGFloat = 4 static let fontSize: CGFloat = 12.5 static let checkmarkWidth: CGFloat = 14 static let itemSpacing: CGFloat = 8 static let itemHorizontalPadding: CGFloat = 8 static let popupPadding: CGFloat = 5 static let screenMargin: CGFloat = 24 + static let maximumPopupHeight: CGFloat = 360 +} + +private struct LitheSettingsControlChrome: ViewModifier { + let background: Color + let border: Color + let cornerRadius: CGFloat + + func body(content: Content) -> some View { + content + .background { + RoundedRectangle(cornerRadius: cornerRadius) + .fill(background) + } + .overlay { + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(border, lineWidth: 1) + } + } } struct LitheSettingsSearchField: View { @@ -50,12 +71,7 @@ struct LitheSettingsSearchField: View { } .padding(.horizontal, 9) .frame(height: 28) - .background(Color.clear) - .clipShape(RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)) - .overlay { - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .stroke(LitheTheme.inputBorder, lineWidth: 1) - } + .litheSettingsControlChrome(background: .clear) .onChange(of: text) { value in onTextChanged?(value) } @@ -73,7 +89,8 @@ struct LitheSettingsSelect: View { private let isAvailable: (Value) -> Bool private let onUnavailableSelection: ((Value) -> Void)? @State private var isPresented = false - @State private var availablePopupWidth: CGFloat? + @State private var popupID = UUID() + @State private var popupAnchor = LitheSettingsSelectAnchorReference() init( selection: Binding, @@ -97,14 +114,11 @@ struct LitheSettingsSelect: View { var body: some View { Button { - if !isPresented { - // Capture the presenting screen before the popover can become the key window. - let screen = NSApp.keyWindow?.screen ?? NSScreen.main - availablePopupWidth = screen.map { - max(1, $0.visibleFrame.width - 2 * SettingsSelectMetrics.screenMargin) - } + if isPresented { + LitheSettingsSelectPopupPresenter.shared.dismiss(ownerID: popupID) + } else { + showPopup() } - isPresented.toggle() } label: { HStack(spacing: 8) { Text(LocalizedStringKey(title(selection))) @@ -120,68 +134,71 @@ struct LitheSettingsSelect: View { .rotationEffect(.degrees(isPresented ? 180 : 0)) } .padding(.horizontal, 9) - .frame(width: width, height: 30, alignment: .leading) - .background( - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .fill(LitheTheme.settingsControlBackground) - ) - .overlay { - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .stroke(isPresented ? LitheTheme.inputFocusBorder : LitheTheme.inputBorder, lineWidth: 1) - } + .frame(width: width, height: SettingsSelectMetrics.controlHeight, alignment: .leading) + .litheSettingsControlChrome(border: isPresented ? LitheTheme.settingsControlAccent : LitheTheme.settingsControlBorder) + .background(LitheSettingsSelectAnchorView(reference: popupAnchor)) .contentShape(Rectangle()) } .buttonStyle(.plain) .lithePointer() .accessibilityLabel(Text(LocalizedStringKey(accessibilityLabel))) - .popover(isPresented: $isPresented, arrowEdge: .bottom) { - VStack(spacing: 2) { - ForEach(options, id: \.self) { option in - Button { - if isAvailable(option) { - selection = option - } else { - onUnavailableSelection?(option) - } - isPresented = false - } label: { - HStack(spacing: SettingsSelectMetrics.itemSpacing) { - Image(systemName: "checkmark") - .font(.system(size: 10, weight: .semibold)) - .foregroundStyle(LitheTheme.accent) - .frame(width: SettingsSelectMetrics.checkmarkWidth) - .opacity(selection == option ? 1 : 0) - - Text(LocalizedStringKey(title(option))) - .font(.system(size: SettingsSelectMetrics.fontSize)) - .foregroundStyle(isAvailable(option) ? LitheTheme.primaryText : LitheTheme.tertiaryText) - .lineLimit(expandsToFitOptions ? nil : 1) - .fixedSize(horizontal: false, vertical: true) - .multilineTextAlignment(.leading) - - Spacer(minLength: SettingsSelectMetrics.itemSpacing) - } - .padding(.horizontal, SettingsSelectMetrics.itemHorizontalPadding) - .padding(.vertical, expandsToFitOptions ? 4 : 0) - .frame(maxWidth: .infinity, minHeight: 28, alignment: .leading) - .litheRowHover( - isActive: selection == option, - activeBackground: LitheTheme.subtleSelection.opacity(isAvailable(option) ? 1 : 0.35) - ) - .contentShape(Rectangle()) - } - .buttonStyle(LitheTreeRowButtonStyle()) - .lithePointer() - .help(isAvailable(option) ? "" : "Shell is not available at this path") - } + .onChange(of: options) { _ in + if isPresented { showPopup() } + } + .onDisappear { + LitheSettingsSelectPopupPresenter.shared.dismiss(ownerID: popupID) + } + } + + private func showPopup() { + guard let anchor = popupAnchor.view, let window = anchor.window else { return } + let anchorFrame = window.convertToScreen(anchor.convert(anchor.bounds, to: nil)) + let visibleFrame = window.screen?.visibleFrame ?? NSScreen.main?.visibleFrame ?? anchorFrame + let popupWidth = preferredPopupWidth(maximumWidth: max(1, visibleFrame.width - SettingsSelectMetrics.screenMargin)) + let state = LitheSettingsSelectPopupState( + selectedIndex: options.firstIndex(of: selection) ?? 0, + optionCount: options.count + ) { index in + let option = options[index] + if isAvailable(option) { + selection = option + } else { + onUnavailableSelection?(option) } - .padding(SettingsSelectMetrics.popupPadding) - .frame(width: preferredPopupWidth) - .lithePopupChrome(cornerRadius: LitheTheme.Metrics.controlCornerRadius) + LitheSettingsSelectPopupPresenter.shared.dismiss(ownerID: popupID) } + let content = LitheSettingsSelectPopupContent( + state: state, + options: options, + selected: selection, + width: popupWidth, + title: title, + expandsToFitOptions: expandsToFitOptions, + isAvailable: isAvailable + ) + let measured = NSHostingView(rootView: content.rows.environment(\.locale, locale)) + let popupHeight = min( + measured.fittingSize.height, + SettingsSelectMetrics.maximumPopupHeight, + max(1, visibleFrame.height - SettingsSelectMetrics.screenMargin) + ) + state.popupHeight = popupHeight + let popup = content.environment(\.locale, locale) + LitheSettingsSelectPopupPresenter.shared.show( + ownerID: popupID, + content: AnyView(popup), + state: state, + anchorFrame: anchorFrame, + size: NSSize(width: popupWidth, height: popupHeight), + visibleFrame: visibleFrame, + appearance: window.effectiveAppearance + ) { + isPresented = false + } + isPresented = true } - private var preferredPopupWidth: CGFloat { + private func preferredPopupWidth(maximumWidth: CGFloat) -> CGFloat { guard expandsToFitOptions else { return width } let font = NSFont.systemFont(ofSize: SettingsSelectMetrics.fontSize) let titleWidth = options.reduce(CGFloat.zero) { widest, option in @@ -194,8 +211,252 @@ struct LitheSettingsSelect: View { + 2 * SettingsSelectMetrics.itemHorizontalPadding + 2 * SettingsSelectMetrics.popupPadding let contentWidth = max(width, ceil(titleWidth) + chromeWidth) - // Derive the width from current titles so discovery updates also resize an open popover. - return min(contentWidth, availablePopupWidth ?? width) + // Derive the width from current titles when discovery refreshes an open list. + return min(contentWidth, maximumWidth) + } +} + +private final class LitheSettingsSelectAnchorReference { + weak var view: NSView? +} + +private struct LitheSettingsSelectAnchorView: NSViewRepresentable { + let reference: LitheSettingsSelectAnchorReference + + func makeNSView(context: Context) -> NSView { + let view = LitheSettingsSelectAnchorNSView() + reference.view = view + return view + } + + func updateNSView(_ view: NSView, context: Context) { + reference.view = view + } +} + +private final class LitheSettingsSelectAnchorNSView: NSView { + override func hitTest(_ point: NSPoint) -> NSView? { nil } +} + +@MainActor +private final class LitheSettingsSelectPopupState: ObservableObject { + @Published var highlightedIndex: Int + @Published var popupHeight: CGFloat = 0 + let optionCount: Int + let onChoose: (Int) -> Void + + init(selectedIndex: Int, optionCount: Int, onChoose: @escaping (Int) -> Void) { + highlightedIndex = selectedIndex + self.optionCount = optionCount + self.onChoose = onChoose + } + + func handleKey(_ event: NSEvent, dismiss: () -> Void) -> Bool { + switch event.keyCode { + case 125, 126: // Down / Up + guard optionCount > 0 else { return true } + highlightedIndex = (highlightedIndex + (event.keyCode == 125 ? 1 : optionCount - 1)) % optionCount + case 36, 76: // Return / keypad Enter + guard optionCount > 0 else { return true } + onChoose(highlightedIndex) + case 53: // Escape + dismiss() + default: + return false + } + return true + } +} + +private struct LitheSettingsSelectPopupContent: View { + @ObservedObject var state: LitheSettingsSelectPopupState + let options: [Value] + let selected: Value + let width: CGFloat + let title: (Value) -> String + let expandsToFitOptions: Bool + let isAvailable: (Value) -> Bool + + var body: some View { + ScrollViewReader { proxy in + ScrollView(.vertical, showsIndicators: false) { + rows + } + .onAppear { proxy.scrollTo(state.highlightedIndex, anchor: .center) } + .onChange(of: state.highlightedIndex) { index in + proxy.scrollTo(index, anchor: .center) + } + } + .scrollContentBackground(.hidden) + .frame(width: width, height: state.popupHeight) + .litheSettingsControlChrome( + background: LitheTheme.settingsPopupBackground, + border: LitheTheme.settingsPopupBorder, + cornerRadius: SettingsSelectMetrics.controlCornerRadius + ) + .clipShape(RoundedRectangle(cornerRadius: SettingsSelectMetrics.controlCornerRadius)) + } + + var rows: some View { + VStack(spacing: 2) { + ForEach(Array(options.enumerated()), id: \.offset) { index, option in + Button { + state.onChoose(index) + } label: { + HStack(spacing: SettingsSelectMetrics.itemSpacing) { + Image(systemName: "checkmark") + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(LitheTheme.settingsControlAccent) + .frame(width: SettingsSelectMetrics.checkmarkWidth) + .opacity(selected == option ? 1 : 0) + + Text(LocalizedStringKey(title(option))) + .font(.system(size: SettingsSelectMetrics.fontSize)) + .foregroundStyle(isAvailable(option) ? LitheTheme.primaryText : LitheTheme.tertiaryText) + .lineLimit(expandsToFitOptions ? nil : 1) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.leading) + + Spacer(minLength: SettingsSelectMetrics.itemSpacing) + } + .padding(.horizontal, SettingsSelectMetrics.itemHorizontalPadding) + .padding(.vertical, expandsToFitOptions ? 4 : 0) + .frame(maxWidth: .infinity, minHeight: SettingsSelectMetrics.controlHeight, alignment: .leading) + .litheRowHover( + isActive: state.highlightedIndex == index, + activeBackground: LitheTheme.subtleSelection.opacity(isAvailable(option) ? 1 : 0.35) + ) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .onHover { hovering in + if hovering { state.highlightedIndex = index } + } + .id(index) + .help(isAvailable(option) ? "" : "Shell is not available at this path") + } + } + .padding(SettingsSelectMetrics.popupPadding) + .frame(width: width) + } +} + +struct LitheSettingsSelectPopupGeometry { + static func frame(anchor: NSRect, size: NSSize, visibleFrame: NSRect) -> NSRect { + let bounds = visibleFrame.insetBy(dx: 6, dy: 6) + let width = min(size.width, bounds.width) + let height = min(size.height, bounds.height) + let below = anchor.minY - bounds.minY - 2 + let above = bounds.maxY - anchor.maxY - 2 + let preferredY = below >= height || below >= above + ? anchor.minY - height - 2 + : anchor.maxY + 2 + return NSRect( + x: min(max(anchor.minX, bounds.minX), bounds.maxX - width), + y: min(max(preferredY, bounds.minY), bounds.maxY - height), + width: width, + height: height + ) + } +} + +@MainActor +private final class LitheSettingsSelectPopupPanel: NSPanel { + var handleKey: ((NSEvent) -> Bool)? + override var canBecomeKey: Bool { true } + + override func sendEvent(_ event: NSEvent) { + if event.type == .keyDown, handleKey?(event) == true { return } + super.sendEvent(event) + } +} + +@MainActor +private final class LitheSettingsSelectPopupPresenter: NSObject, NSWindowDelegate { + static let shared = LitheSettingsSelectPopupPresenter() + + private var panel: LitheSettingsSelectPopupPanel? + private var ownerID: UUID? + private var onDismiss: (() -> Void)? + private var localEventMonitor: Any? + private var globalEventMonitor: Any? + + func show( + ownerID: UUID, + content: AnyView, + state: LitheSettingsSelectPopupState, + anchorFrame: NSRect, + size: NSSize, + visibleFrame: NSRect, + appearance: NSAppearance, + onDismiss: @escaping () -> Void + ) { + dismiss() + let frame = LitheSettingsSelectPopupGeometry.frame( + anchor: anchorFrame, size: size, visibleFrame: visibleFrame + ) + let panel = LitheSettingsSelectPopupPanel( + contentRect: frame, + styleMask: [.borderless, .nonactivatingPanel], + backing: .buffered, + defer: false + ) + panel.handleKey = { [weak self, weak state] event in + state?.handleKey(event) { self?.dismiss(ownerID: ownerID) } ?? false + } + panel.contentViewController = NSHostingController(rootView: content) + panel.appearance = appearance + panel.animationBehavior = .none + panel.backgroundColor = .clear + panel.isOpaque = false + panel.hasShadow = true + panel.level = .popUpMenu + panel.isFloatingPanel = true + panel.hidesOnDeactivate = true + panel.collectionBehavior = [.transient, .fullScreenAuxiliary] + panel.delegate = self + self.panel = panel + self.ownerID = ownerID + self.onDismiss = onDismiss + installEventMonitors() + panel.orderFrontRegardless() + panel.makeKey() + } + + func dismiss(ownerID: UUID? = nil) { + guard ownerID == nil || ownerID == self.ownerID else { return } + removeEventMonitors() + let callback = onDismiss + onDismiss = nil + self.ownerID = nil + let closingPanel = panel + panel = nil + closingPanel?.delegate = nil + closingPanel?.orderOut(nil) + closingPanel?.close() + callback?() + } + + func windowDidResignKey(_ notification: Notification) { + dismiss() + } + + private func installEventMonitors() { + localEventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] event in + guard let self else { return event } + if event.window !== self.panel { self.dismiss() } + return event + } + globalEventMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] _ in + self?.dismiss() + } + } + + private func removeEventMonitors() { + if let localEventMonitor { NSEvent.removeMonitor(localEventMonitor) } + if let globalEventMonitor { NSEvent.removeMonitor(globalEventMonitor) } + localEventMonitor = nil + globalEventMonitor = nil } } @@ -225,12 +486,12 @@ struct LitheSettingsSegmentedControl: View { } label: { Text(LocalizedStringKey(title(option))) .font(.system(size: 12, weight: .medium)) - .foregroundStyle(selection == option ? Color.white : LitheTheme.secondaryText) - .frame(maxWidth: .infinity, minHeight: 26) + .foregroundStyle(selection == option ? LitheTheme.settingsSelectionText : LitheTheme.secondaryText) + .frame(maxWidth: .infinity, minHeight: 24) .contentShape(Rectangle()) .litheRowHover( isActive: selection == option, - cornerRadius: LitheTheme.Metrics.cornerRadius, + cornerRadius: SettingsSelectMetrics.controlCornerRadius, activeBackground: LitheTheme.settingsSelection ) } @@ -239,15 +500,8 @@ struct LitheSettingsSegmentedControl: View { } } .padding(2) - .frame(width: width, height: 30) - .background( - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .fill(LitheTheme.settingsControlBackground) - ) - .overlay { - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .stroke(LitheTheme.inputBorder, lineWidth: 1) - } + .frame(width: width, height: SettingsSelectMetrics.controlHeight) + .litheSettingsControlChrome() } } @@ -274,16 +528,17 @@ struct LitheSettingsCheckbox: View { } label: { HStack(spacing: 8) { ZStack { - RoundedRectangle(cornerRadius: 4) - .fill(isOn ? LitheTheme.accent : LitheTheme.inputBackground) - RoundedRectangle(cornerRadius: 4) - .stroke(isOn ? LitheTheme.accent : LitheTheme.inputBorder, lineWidth: 1) Image(systemName: "checkmark") .font(.system(size: 9, weight: .bold)) .foregroundStyle(Color.white) .opacity(isOn ? 1 : 0) } .frame(width: 16, height: 16) + .litheSettingsControlChrome( + background: isOn ? LitheTheme.settingsControlAccent : LitheTheme.settingsControlBackground, + border: isOn ? LitheTheme.settingsControlAccent : LitheTheme.settingsControlBorder, + cornerRadius: 3 + ) if let title { Text(title) @@ -333,8 +588,8 @@ struct LitheSettingsStepper: View where Value: Strideable & Comparable, V .padding(.horizontal, 8) Rectangle() - .fill(LitheTheme.inputBorder) - .frame(width: 1, height: 20) + .fill(LitheTheme.settingsControlBorder) + .frame(width: 1, height: 18) stepButton(systemImage: "minus", isDisabled: value <= range.lowerBound) { value = max(range.lowerBound, value.advanced(by: -step)) @@ -344,16 +599,9 @@ struct LitheSettingsStepper: View where Value: Strideable & Comparable, V value = min(range.upperBound, value.advanced(by: step)) } } - .frame(width: width, height: 30) - .background( - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .fill(LitheTheme.inputBackground) - ) - .overlay { - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .stroke(LitheTheme.inputBorder, lineWidth: 1) - } - .clipShape(RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)) + .frame(width: width, height: SettingsSelectMetrics.controlHeight) + .litheSettingsControlChrome() + .clipShape(RoundedRectangle(cornerRadius: SettingsSelectMetrics.controlCornerRadius)) .accessibilityElement(children: .contain) .accessibilityLabel(Text(accessibilityLabel)) } @@ -367,7 +615,7 @@ struct LitheSettingsStepper: View where Value: Strideable & Comparable, V Image(systemName: systemImage) .font(.system(size: 9, weight: .semibold)) .foregroundStyle(isDisabled ? LitheTheme.tertiaryText : LitheTheme.secondaryText) - .frame(width: 26, height: 28) + .frame(width: 26, height: 26) .contentShape(Rectangle()) .litheRowHover(cornerRadius: 0) } @@ -385,21 +633,27 @@ private struct LitheSettingsTextFieldModifier: ViewModifier { .textFieldStyle(.plain) .font(.system(size: 12.5)) .padding(.horizontal, 9) - .frame(height: 30) - .background( - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .fill(LitheTheme.inputBackground) - ) - .overlay { - RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius) - .stroke(LitheTheme.inputBorder, lineWidth: 1) - } + .frame(height: SettingsSelectMetrics.controlHeight) + .litheSettingsControlChrome(background: LitheTheme.settingsTextFieldBackground) .opacity(isEnabled ? 1 : 0.55) } } extension View { + func litheSettingsControlChrome( + background: Color = LitheTheme.settingsControlBackground, + border: Color = LitheTheme.settingsControlBorder, + cornerRadius: CGFloat = SettingsSelectMetrics.controlCornerRadius + ) -> some View { + modifier(LitheSettingsControlChrome(background: background, border: border, cornerRadius: cornerRadius)) + } + func litheSettingsTextField() -> some View { modifier(LitheSettingsTextFieldModifier()) } + + func litheSettingsTextEditor() -> some View { + scrollContentBackground(.hidden) + .litheSettingsControlChrome(background: LitheTheme.settingsTextFieldBackground) + } } diff --git a/macos/Sources/Lithe/Views/Language/LSPControlCenterView.swift b/macos/Sources/Lithe/Views/Language/LSPControlCenterView.swift index cd7211a3e..d5550aee1 100644 --- a/macos/Sources/Lithe/Views/Language/LSPControlCenterView.swift +++ b/macos/Sources/Lithe/Views/Language/LSPControlCenterView.swift @@ -8,7 +8,6 @@ struct LSPControlCenterView: View { var body: some View { VStack(spacing: 0) { - header ScrollView { VStack(alignment: .leading, spacing: 16) { projectSummary @@ -23,7 +22,8 @@ struct LSPControlCenterView: View { degradedCatalogNotice } } - .padding(20) + .padding(.horizontal, 16) + .padding(.bottom, 20) .frame(maxWidth: .infinity, alignment: .leading) } .litheScrollViewChrome(alwaysShowVertical: true, usesCompactScrollers: true) @@ -32,18 +32,6 @@ struct LSPControlCenterView: View { .background(LitheTheme.settingsSurface) } - private var header: some View { - HStack { - Text(usesChinese ? "语言支持" : "Language Support") - .font(.system(size: 15, weight: .semibold)) - .foregroundStyle(LitheTheme.primaryText) - Spacer() - } - .padding(.horizontal, 16) - .frame(height: 42) - .background(LitheTheme.settingsSurface) - } - private var projectSummary: some View { VStack(alignment: .leading, spacing: 5) { Text(model.projectName) diff --git a/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift b/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift new file mode 100644 index 000000000..12265c631 --- /dev/null +++ b/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift @@ -0,0 +1,29 @@ +import CoreGraphics +import Testing +@testable import Lithe + +@Suite("Settings select popup geometry") +struct SettingsSelectPopupGeometryTests { + @Test + func opensBelowTheControlWithoutAnArrowGap() { + let anchor = CGRect(x: 100, y: 500, width: 190, height: 28) + let frame = LitheSettingsSelectPopupGeometry.frame( + anchor: anchor, + size: CGSize(width: 190, height: 90), + visibleFrame: CGRect(x: 0, y: 0, width: 1000, height: 800) + ) + #expect(frame == CGRect(x: 100, y: 408, width: 190, height: 90)) + } + + @Test + func flipsAboveAndClampsToTheVisibleScreen() { + let visible = CGRect(x: 0, y: 0, width: 1000, height: 800) + let frame = LitheSettingsSelectPopupGeometry.frame( + anchor: CGRect(x: 950, y: 30, width: 190, height: 28), + size: CGSize(width: 190, height: 90), + visibleFrame: visible + ) + #expect(frame == CGRect(x: 804, y: 60, width: 190, height: 90)) + #expect(visible.contains(frame)) + } +} diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index f8308ed12..1c1cad1fb 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -1747,7 +1747,9 @@ "macos/Sources/Lithe/LitheApp.swift", "macos/Sources/Lithe/Views/App/SettingsView.swift", "macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift", - "macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift" + "macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift", + "macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift", + "macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift" ], "implementationStatus": "implemented", "verificationStatus": "pending" @@ -1760,7 +1762,7 @@ "verificationStatus": "pending" }, "owner": "Settings", - "verification": "修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 保持设置窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。" + "verification": "修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。" }, { "id": "keymap-customization", From 493e09574927d2f296c76429581521cebd8b7071 Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 11:20:14 +0800 Subject: [PATCH 02/19] style(macos): match IntelliJ settings dropdown popup --- docs/development/platform-parity-matrix.csv | 2 +- docs/development/platform-parity-matrix.md | 2 +- .../Components/LitheSettingsControls.swift | 42 ++++++++----------- shared/platform-feature-matrix.json | 2 +- 4 files changed, 21 insertions(+), 27 deletions(-) diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv index 8222abe79..0270db0bf 100644 --- a/docs/development/platform-parity-matrix.csv +++ b/docs/development/platform-parity-matrix.csv @@ -69,7 +69,7 @@ outline-symbols,编辑器,代码结构,当前文件 Outline 与符号树,部分 references-pane,编辑器,代码结构,引用结果面板与引用跳转,已实现,待验证,已实现,待验证,Language Tooling,从符号发起引用查询,验证结果分组、文件定位、关闭和重新查询。,,macos/Sources/Lithe/Views/Language/JavaReferencesView.swift,windows/tauri/src/features/references diagnostics-workspace,可观测性,诊断与日志,工作区诊断与问题面板,已实现,待验证,已实现,待验证,Diagnostics,制造多个来源的错误和警告,验证筛选、排序、定位和清除。,,macos/Sources/Lithe/Application/Features/DiagnosticsFeatureModel.swift; macos/Sources/Lithe/Views/Language/JavaProblemsView.swift,windows/tauri/src/features/diagnostics; windows/tauri/src/features/logging; windows/tauri/src/ui/dialog.tsx diagnostics-export,可观测性,诊断与日志,诊断包导出与脱敏,已实现,待验证,已实现,待验证,Diagnostics,导出诊断包,确认敏感路径、凭据和大文件被正确脱敏或限制;检查预览说明和文件列表在确认弹窗内完整显示、长内容正常换行且不溢出。,,macos/Sources/Lithe/Views/Diagnostics/DiagnosticsExportSheet.swift; macos/Sources/Lithe/Services/Diagnostics,windows/tauri/src/features/diagnostics -application-settings,设置,应用配置,应用、项目与运行设置,已实现,待验证,已实现,待验证,Settings,修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift; macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift,windows/tauri/src/features/settings +application-settings,设置,应用配置,应用、项目与运行设置,已实现,待验证,已实现,待验证,Settings,修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、键盘选择与滚动;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift; macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift,windows/tauri/src/features/settings keymap-customization,设置,应用配置,快捷键查看与自定义,已实现,待验证,已实现,待验证,Settings,从设置 → 快捷键打开编辑器,按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。,Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。,macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift; macos/Sources/Lithe/Models/Keymap,windows/tauri/src/features/keymaps; windows/tauri/src/features/settings/components/macos-settings-panels.tsx; windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx; windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx plugin-extension-management,扩展,插件与主题,插件、语言服务与主题管理,已实现,待验证,已实现,待验证,Extensions,启用、禁用和恢复插件/主题,确认启动加载、失败隔离和设置持久化。,,macos/Sources/Lithe/Views/App/PluginManagementView.swift; macos/Sources/Lithe/Platform/MacOS/Plugins,windows/tauri/src/extensions; windows/tauri/src/features/settings onboarding-welcome,启动与窗口,首次启动,欢迎页、项目入口与首次启动引导,已实现,待验证,已实现,待验证,Onboarding,清空首次启动状态,验证打开项目、克隆项目、最近项目和跳过引导。,,macos/Sources/Lithe/Views/App/WelcomeView.swift; macos/Sources/Lithe/Views/Workspace,windows/tauri/src/features/onboarding; windows/tauri/src/features/bootstrap diff --git a/docs/development/platform-parity-matrix.md b/docs/development/platform-parity-matrix.md index b41a29c81..2c62d921a 100644 --- a/docs/development/platform-parity-matrix.md +++ b/docs/development/platform-parity-matrix.md @@ -204,7 +204,7 @@ | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | -| 应用配置 | **应用、项目与运行设置**
application-settings | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift`、`macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift`、`macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings` | Settings | 修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。 | | +| 应用配置 | **应用、项目与运行设置**
application-settings | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift`、`macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift`、`macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings` | Settings | 修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、键盘选择与滚动;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。 | | | 应用配置 | **快捷键查看与自定义**
keymap-customization | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift`、`macos/Sources/Lithe/Models/Keymap` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/keymaps`、`windows/tauri/src/features/settings/components/macos-settings-panels.tsx`、`windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx`、`windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx` | Settings | 从设置 → 快捷键打开编辑器,按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。 | Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。 | diff --git a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift index e4ea84dda..ea6e29558 100644 --- a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift +++ b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift @@ -5,12 +5,12 @@ private enum SettingsSelectMetrics { static let controlHeight: CGFloat = 28 static let controlCornerRadius: CGFloat = 4 static let fontSize: CGFloat = 12.5 - static let checkmarkWidth: CGFloat = 14 - static let itemSpacing: CGFloat = 8 + static let popupCornerRadius: CGFloat = 8 + static let itemHeight: CGFloat = 24 static let itemHorizontalPadding: CGFloat = 8 - static let popupPadding: CGFloat = 5 + static let popupPadding: CGFloat = 6 static let screenMargin: CGFloat = 24 - static let maximumPopupHeight: CGFloat = 360 + static let maximumPopupHeight: CGFloat = 10 * itemHeight + 2 * popupPadding } private struct LitheSettingsControlChrome: ViewModifier { @@ -170,7 +170,6 @@ struct LitheSettingsSelect: View { let content = LitheSettingsSelectPopupContent( state: state, options: options, - selected: selection, width: popupWidth, title: title, expandsToFitOptions: expandsToFitOptions, @@ -205,10 +204,7 @@ struct LitheSettingsSelect: View { let text = String(localized: String.LocalizationValue(title(option)), locale: locale) return max(widest, (text as NSString).size(withAttributes: [.font: font]).width) } - // Include the checkmark, both HStack gaps, trailing spacer, and both layers of padding. - let chromeWidth = SettingsSelectMetrics.checkmarkWidth - + 3 * SettingsSelectMetrics.itemSpacing - + 2 * SettingsSelectMetrics.itemHorizontalPadding + let chromeWidth = 2 * SettingsSelectMetrics.itemHorizontalPadding + 2 * SettingsSelectMetrics.popupPadding let contentWidth = max(width, ceil(titleWidth) + chromeWidth) // Derive the width from current titles when discovery refreshes an open list. @@ -271,7 +267,6 @@ private final class LitheSettingsSelectPopupState: ObservableObject { private struct LitheSettingsSelectPopupContent: View { @ObservedObject var state: LitheSettingsSelectPopupState let options: [Value] - let selected: Value let width: CGFloat let title: (Value) -> String let expandsToFitOptions: Bool @@ -292,39 +287,38 @@ private struct LitheSettingsSelectPopupContent: View { .litheSettingsControlChrome( background: LitheTheme.settingsPopupBackground, border: LitheTheme.settingsPopupBorder, - cornerRadius: SettingsSelectMetrics.controlCornerRadius + cornerRadius: SettingsSelectMetrics.popupCornerRadius ) - .clipShape(RoundedRectangle(cornerRadius: SettingsSelectMetrics.controlCornerRadius)) + .clipShape(RoundedRectangle(cornerRadius: SettingsSelectMetrics.popupCornerRadius)) } var rows: some View { - VStack(spacing: 2) { + VStack(spacing: 0) { ForEach(Array(options.enumerated()), id: \.offset) { index, option in Button { state.onChoose(index) } label: { - HStack(spacing: SettingsSelectMetrics.itemSpacing) { - Image(systemName: "checkmark") - .font(.system(size: 10, weight: .semibold)) - .foregroundStyle(LitheTheme.settingsControlAccent) - .frame(width: SettingsSelectMetrics.checkmarkWidth) - .opacity(selected == option ? 1 : 0) - + HStack { Text(LocalizedStringKey(title(option))) .font(.system(size: SettingsSelectMetrics.fontSize)) - .foregroundStyle(isAvailable(option) ? LitheTheme.primaryText : LitheTheme.tertiaryText) + .foregroundStyle( + isAvailable(option) + ? (state.highlightedIndex == index ? LitheTheme.settingsSelectionText : LitheTheme.primaryText) + : LitheTheme.tertiaryText + ) .lineLimit(expandsToFitOptions ? nil : 1) .fixedSize(horizontal: false, vertical: true) .multilineTextAlignment(.leading) - Spacer(minLength: SettingsSelectMetrics.itemSpacing) + Spacer(minLength: 0) } .padding(.horizontal, SettingsSelectMetrics.itemHorizontalPadding) .padding(.vertical, expandsToFitOptions ? 4 : 0) - .frame(maxWidth: .infinity, minHeight: SettingsSelectMetrics.controlHeight, alignment: .leading) + .frame(maxWidth: .infinity, minHeight: SettingsSelectMetrics.itemHeight, alignment: .leading) .litheRowHover( isActive: state.highlightedIndex == index, - activeBackground: LitheTheme.subtleSelection.opacity(isAvailable(option) ? 1 : 0.35) + cornerRadius: SettingsSelectMetrics.controlCornerRadius, + activeBackground: LitheTheme.settingsSelection.opacity(isAvailable(option) ? 1 : 0.35) ) .contentShape(Rectangle()) } diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index 1c1cad1fb..7ecb4cb16 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -1762,7 +1762,7 @@ "verificationStatus": "pending" }, "owner": "Settings", - "verification": "修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。" + "verification": "修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、键盘选择与滚动;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。" }, { "id": "keymap-customization", From a17dc243bb0f1daddfd7c0c35e4db491b5f56630 Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 11:35:09 +0800 Subject: [PATCH 03/19] fix(macos): stabilize settings popup and accessibility --- docs/development/platform-parity-matrix.csv | 2 +- docs/development/platform-parity-matrix.md | 2 +- .../Components/LitheSettingsControls.swift | 24 ++++++++++++------- .../SettingsSelectPopupGeometryTests.swift | 12 ++++++++++ shared/platform-feature-matrix.json | 2 +- 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv index 0270db0bf..5bddb3c54 100644 --- a/docs/development/platform-parity-matrix.csv +++ b/docs/development/platform-parity-matrix.csv @@ -69,7 +69,7 @@ outline-symbols,编辑器,代码结构,当前文件 Outline 与符号树,部分 references-pane,编辑器,代码结构,引用结果面板与引用跳转,已实现,待验证,已实现,待验证,Language Tooling,从符号发起引用查询,验证结果分组、文件定位、关闭和重新查询。,,macos/Sources/Lithe/Views/Language/JavaReferencesView.swift,windows/tauri/src/features/references diagnostics-workspace,可观测性,诊断与日志,工作区诊断与问题面板,已实现,待验证,已实现,待验证,Diagnostics,制造多个来源的错误和警告,验证筛选、排序、定位和清除。,,macos/Sources/Lithe/Application/Features/DiagnosticsFeatureModel.swift; macos/Sources/Lithe/Views/Language/JavaProblemsView.swift,windows/tauri/src/features/diagnostics; windows/tauri/src/features/logging; windows/tauri/src/ui/dialog.tsx diagnostics-export,可观测性,诊断与日志,诊断包导出与脱敏,已实现,待验证,已实现,待验证,Diagnostics,导出诊断包,确认敏感路径、凭据和大文件被正确脱敏或限制;检查预览说明和文件列表在确认弹窗内完整显示、长内容正常换行且不溢出。,,macos/Sources/Lithe/Views/Diagnostics/DiagnosticsExportSheet.swift; macos/Sources/Lithe/Services/Diagnostics,windows/tauri/src/features/diagnostics -application-settings,设置,应用配置,应用、项目与运行设置,已实现,待验证,已实现,待验证,Settings,修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、键盘选择与滚动;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift; macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift,windows/tauri/src/features/settings +application-settings,设置,应用配置,应用、项目与运行设置,已实现,待验证,已实现,待验证,Settings,修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift; macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift,windows/tauri/src/features/settings keymap-customization,设置,应用配置,快捷键查看与自定义,已实现,待验证,已实现,待验证,Settings,从设置 → 快捷键打开编辑器,按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。,Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。,macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift; macos/Sources/Lithe/Models/Keymap,windows/tauri/src/features/keymaps; windows/tauri/src/features/settings/components/macos-settings-panels.tsx; windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx; windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx plugin-extension-management,扩展,插件与主题,插件、语言服务与主题管理,已实现,待验证,已实现,待验证,Extensions,启用、禁用和恢复插件/主题,确认启动加载、失败隔离和设置持久化。,,macos/Sources/Lithe/Views/App/PluginManagementView.swift; macos/Sources/Lithe/Platform/MacOS/Plugins,windows/tauri/src/extensions; windows/tauri/src/features/settings onboarding-welcome,启动与窗口,首次启动,欢迎页、项目入口与首次启动引导,已实现,待验证,已实现,待验证,Onboarding,清空首次启动状态,验证打开项目、克隆项目、最近项目和跳过引导。,,macos/Sources/Lithe/Views/App/WelcomeView.swift; macos/Sources/Lithe/Views/Workspace,windows/tauri/src/features/onboarding; windows/tauri/src/features/bootstrap diff --git a/docs/development/platform-parity-matrix.md b/docs/development/platform-parity-matrix.md index 2c62d921a..c664d9a5e 100644 --- a/docs/development/platform-parity-matrix.md +++ b/docs/development/platform-parity-matrix.md @@ -204,7 +204,7 @@ | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | -| 应用配置 | **应用、项目与运行设置**
application-settings | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift`、`macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift`、`macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings` | Settings | 修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、键盘选择与滚动;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。 | | +| 应用配置 | **应用、项目与运行设置**
application-settings | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift`、`macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift`、`macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings` | Settings | 修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。 | | | 应用配置 | **快捷键查看与自定义**
keymap-customization | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift`、`macos/Sources/Lithe/Models/Keymap` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/keymaps`、`windows/tauri/src/features/settings/components/macos-settings-panels.tsx`、`windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx`、`windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx` | Settings | 从设置 → 快捷键打开编辑器,按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。 | Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。 | diff --git a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift index ea6e29558..7ea33bed2 100644 --- a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift +++ b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift @@ -181,7 +181,6 @@ struct LitheSettingsSelect: View { SettingsSelectMetrics.maximumPopupHeight, max(1, visibleFrame.height - SettingsSelectMetrics.screenMargin) ) - state.popupHeight = popupHeight let popup = content.environment(\.locale, locale) LitheSettingsSelectPopupPresenter.shared.show( ownerID: popupID, @@ -237,6 +236,7 @@ private final class LitheSettingsSelectAnchorNSView: NSView { @MainActor private final class LitheSettingsSelectPopupState: ObservableObject { @Published var highlightedIndex: Int + @Published var keyboardScrollIndex: Int? @Published var popupHeight: CGFloat = 0 let optionCount: Int let onChoose: (Int) -> Void @@ -252,6 +252,7 @@ private final class LitheSettingsSelectPopupState: ObservableObject { case 125, 126: // Down / Up guard optionCount > 0 else { return true } highlightedIndex = (highlightedIndex + (event.keyCode == 125 ? 1 : optionCount - 1)) % optionCount + keyboardScrollIndex = highlightedIndex case 36, 76: // Return / keypad Enter guard optionCount > 0 else { return true } onChoose(highlightedIndex) @@ -277,9 +278,9 @@ private struct LitheSettingsSelectPopupContent: View { ScrollView(.vertical, showsIndicators: false) { rows } - .onAppear { proxy.scrollTo(state.highlightedIndex, anchor: .center) } - .onChange(of: state.highlightedIndex) { index in - proxy.scrollTo(index, anchor: .center) + .onAppear { proxy.scrollTo(state.highlightedIndex) } + .onChange(of: state.keyboardScrollIndex) { index in + if let index { proxy.scrollTo(index) } } } .scrollContentBackground(.hidden) @@ -339,10 +340,11 @@ struct LitheSettingsSelectPopupGeometry { static func frame(anchor: NSRect, size: NSSize, visibleFrame: NSRect) -> NSRect { let bounds = visibleFrame.insetBy(dx: 6, dy: 6) let width = min(size.width, bounds.width) - let height = min(size.height, bounds.height) - let below = anchor.minY - bounds.minY - 2 - let above = bounds.maxY - anchor.maxY - 2 - let preferredY = below >= height || below >= above + let below = max(0, anchor.minY - bounds.minY - 2) + let above = max(0, bounds.maxY - anchor.maxY - 2) + let opensBelow = below >= size.height || below >= above + let height = min(size.height, opensBelow ? below : above) + let preferredY = opensBelow ? anchor.minY - height - 2 : anchor.maxY + 2 return NSRect( @@ -389,6 +391,7 @@ private final class LitheSettingsSelectPopupPresenter: NSObject, NSWindowDelegat let frame = LitheSettingsSelectPopupGeometry.frame( anchor: anchorFrame, size: size, visibleFrame: visibleFrame ) + state.popupHeight = frame.height let panel = LitheSettingsSelectPopupPanel( contentRect: frame, styleMask: [.borderless, .nonactivatingPanel], @@ -491,6 +494,7 @@ struct LitheSettingsSegmentedControl: View { } .buttonStyle(LitheTreeRowButtonStyle()) .lithePointer() + .accessibilityValue(selection == option ? Text("Selected") : Text("Not selected")) } } .padding(2) @@ -544,7 +548,9 @@ struct LitheSettingsCheckbox: View { } .buttonStyle(LitheTreeRowButtonStyle()) .lithePointer() - .accessibilityLabel(Text(accessibilityLabel)) + .accessibilityRepresentation { + Toggle(accessibilityLabel, isOn: $isOn) + } } } diff --git a/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift b/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift index 12265c631..f7f960910 100644 --- a/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift +++ b/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift @@ -26,4 +26,16 @@ struct SettingsSelectPopupGeometryTests { #expect(frame == CGRect(x: 804, y: 60, width: 190, height: 90)) #expect(visible.contains(frame)) } + + @Test + func shrinksToAvailableSpaceWithoutCoveringTheControl() { + let anchor = CGRect(x: 100, y: 246, width: 190, height: 28) + let frame = LitheSettingsSelectPopupGeometry.frame( + anchor: anchor, + size: CGSize(width: 190, height: 252), + visibleFrame: CGRect(x: 0, y: 0, width: 800, height: 500) + ) + #expect(frame == CGRect(x: 100, y: 6, width: 190, height: 238)) + #expect(!frame.intersects(anchor)) + } } diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index 7ecb4cb16..d6b2afad7 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -1762,7 +1762,7 @@ "verificationStatus": "pending" }, "owner": "Settings", - "verification": "修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、键盘选择与滚动;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。" + "verification": "修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。" }, { "id": "keymap-customization", From 18b03b11e8e8fa6272c3e5d1b189e81081e6ecad Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 11:45:59 +0800 Subject: [PATCH 04/19] fix(macos): localize settings breadcrumb categories --- macos/Resources/zh-Hans.lproj/Localizable.strings | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/macos/Resources/zh-Hans.lproj/Localizable.strings b/macos/Resources/zh-Hans.lproj/Localizable.strings index 6c80e14da..f5021acf1 100644 --- a/macos/Resources/zh-Hans.lproj/Localizable.strings +++ b/macos/Resources/zh-Hans.lproj/Localizable.strings @@ -6,6 +6,11 @@ "Try a different search term." = "请尝试其他搜索词。"; "Project" = "项目"; "General" = "通用"; +"Appearance & Behavior" = "外观与行为"; +"Build, Execution, Deployment" = "构建、执行、部署"; +"Languages & Frameworks" = "语言与框架"; +"Version Control" = "版本控制"; +"Tools" = "工具"; "Appearance" = "外观"; "Theme" = "主题"; "Color theme" = "配色主题"; From 4159103413398ad31a5db7e91ab206d47acc7af6 Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 11:56:33 +0800 Subject: [PATCH 05/19] feat(macos): organize keymap settings as action tree --- docs/development/platform-parity-matrix.csv | 2 +- docs/development/platform-parity-matrix.md | 2 +- .../zh-Hans.lproj/Localizable.strings | 2 + .../App/KeyboardShortcutSettingsView.swift | 192 +++++++++++++----- shared/platform-feature-matrix.json | 2 +- 5 files changed, 141 insertions(+), 59 deletions(-) diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv index 5bddb3c54..6688aefd8 100644 --- a/docs/development/platform-parity-matrix.csv +++ b/docs/development/platform-parity-matrix.csv @@ -70,7 +70,7 @@ references-pane,编辑器,代码结构,引用结果面板与引用跳转,已实 diagnostics-workspace,可观测性,诊断与日志,工作区诊断与问题面板,已实现,待验证,已实现,待验证,Diagnostics,制造多个来源的错误和警告,验证筛选、排序、定位和清除。,,macos/Sources/Lithe/Application/Features/DiagnosticsFeatureModel.swift; macos/Sources/Lithe/Views/Language/JavaProblemsView.swift,windows/tauri/src/features/diagnostics; windows/tauri/src/features/logging; windows/tauri/src/ui/dialog.tsx diagnostics-export,可观测性,诊断与日志,诊断包导出与脱敏,已实现,待验证,已实现,待验证,Diagnostics,导出诊断包,确认敏感路径、凭据和大文件被正确脱敏或限制;检查预览说明和文件列表在确认弹窗内完整显示、长内容正常换行且不溢出。,,macos/Sources/Lithe/Views/Diagnostics/DiagnosticsExportSheet.swift; macos/Sources/Lithe/Services/Diagnostics,windows/tauri/src/features/diagnostics application-settings,设置,应用配置,应用、项目与运行设置,已实现,待验证,已实现,待验证,Settings,修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift; macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift,windows/tauri/src/features/settings -keymap-customization,设置,应用配置,快捷键查看与自定义,已实现,待验证,已实现,待验证,Settings,从设置 → 快捷键打开编辑器,按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。,Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。,macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift; macos/Sources/Lithe/Models/Keymap,windows/tauri/src/features/keymaps; windows/tauri/src/features/settings/components/macos-settings-panels.tsx; windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx; windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx +keymap-customization,设置,应用配置,快捷键查看与自定义,已实现,待验证,已实现,待验证,Settings,macOS 从设置 → 快捷键打开分类树,检查初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、工具栏编辑、行内快捷键新增与删除及恢复默认;按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。,Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。,macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift; macos/Sources/Lithe/Models/Keymap,windows/tauri/src/features/keymaps; windows/tauri/src/features/settings/components/macos-settings-panels.tsx; windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx; windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx plugin-extension-management,扩展,插件与主题,插件、语言服务与主题管理,已实现,待验证,已实现,待验证,Extensions,启用、禁用和恢复插件/主题,确认启动加载、失败隔离和设置持久化。,,macos/Sources/Lithe/Views/App/PluginManagementView.swift; macos/Sources/Lithe/Platform/MacOS/Plugins,windows/tauri/src/extensions; windows/tauri/src/features/settings onboarding-welcome,启动与窗口,首次启动,欢迎页、项目入口与首次启动引导,已实现,待验证,已实现,待验证,Onboarding,清空首次启动状态,验证打开项目、克隆项目、最近项目和跳过引导。,,macos/Sources/Lithe/Views/App/WelcomeView.swift; macos/Sources/Lithe/Views/Workspace,windows/tauri/src/features/onboarding; windows/tauri/src/features/bootstrap command-palette,工作台,命令与布局,命令面板与全局动作,已实现,待验证,已实现,待验证,Workbench,搜索并执行打开面板、运行、Git 和设置命令,确认快捷键和不可用命令状态。,,macos/Sources/Lithe/Models/Keymap/LitheCommandCatalog.swift; macos/Sources/Lithe/Views/Search/SearchEverywhereView.swift,windows/tauri/src/features/command-palette diff --git a/docs/development/platform-parity-matrix.md b/docs/development/platform-parity-matrix.md index c664d9a5e..e1666592c 100644 --- a/docs/development/platform-parity-matrix.md +++ b/docs/development/platform-parity-matrix.md @@ -205,7 +205,7 @@ | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | | 应用配置 | **应用、项目与运行设置**
application-settings | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift`、`macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift`、`macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings` | Settings | 修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。 | | -| 应用配置 | **快捷键查看与自定义**
keymap-customization | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift`、`macos/Sources/Lithe/Models/Keymap` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/keymaps`、`windows/tauri/src/features/settings/components/macos-settings-panels.tsx`、`windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx`、`windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx` | Settings | 从设置 → 快捷键打开编辑器,按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。 | Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。 | +| 应用配置 | **快捷键查看与自定义**
keymap-customization | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift`、`macos/Sources/Lithe/Models/Keymap` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/keymaps`、`windows/tauri/src/features/settings/components/macos-settings-panels.tsx`、`windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx`、`windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx` | Settings | macOS 从设置 → 快捷键打开分类树,检查初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、工具栏编辑、行内快捷键新增与删除及恢复默认;按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。 | Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。 | diff --git a/macos/Resources/zh-Hans.lproj/Localizable.strings b/macos/Resources/zh-Hans.lproj/Localizable.strings index f5021acf1..eecd12cec 100644 --- a/macos/Resources/zh-Hans.lproj/Localizable.strings +++ b/macos/Resources/zh-Hans.lproj/Localizable.strings @@ -37,6 +37,8 @@ "Keymap" = "快捷键"; "Customize shortcuts for Lithe actions. Changes apply immediately." = "自定义 Lithe 操作的快捷键,修改后立即生效。"; "Search actions or shortcuts" = "搜索操作或快捷键"; +"Expand All" = "全部展开"; +"Collapse All" = "全部折叠"; "Restore All Defaults" = "全部恢复默认"; "Not Assigned" = "未分配"; "Press shortcut…" = "请按下快捷键…"; diff --git a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift index 9b9ebacc5..f0bbd6673 100644 --- a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift @@ -4,6 +4,8 @@ struct KeyboardShortcutSettingsView: View { @ObservedObject var feature: KeyboardShortcutFeatureModel let language: AppLanguage @State private var query = "" + @State private var expandedGroups: Set = [] + @State private var selectedCommandID: String? @State private var editingTarget: EditingTarget? @State private var validationIssue: ValidationIssue? @@ -21,15 +23,9 @@ struct KeyboardShortcutSettingsView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { header + toolbar ScrollView { - LazyVStack(alignment: .leading, spacing: 18) { - let sections = feature.groupedCommands(query: query) { command in - [ - localizedString(command.title), - localizedString(command.subtitle), - localizedString(command.group.rawValue) - ].joined(separator: " ") - } + LazyVStack(alignment: .leading, spacing: 0) { if sections.isEmpty { emptyState } else { @@ -38,77 +34,155 @@ struct KeyboardShortcutSettingsView: View { } } } - .padding(.horizontal, 16) - .padding(.bottom, 20) + .padding(.horizontal, 12) + .padding(.vertical, 6) } .litheScrollViewChrome(alwaysShowVertical: true, usesCompactScrollers: true) } .background(LitheTheme.settingsSurface) } + private var sections: [KeyboardShortcutCommandSection] { + feature.groupedCommands(query: query) { command in + [ + localizedString(command.title), + localizedString(command.subtitle), + localizedString(command.group.rawValue) + ].joined(separator: " ") + } + } + private var header: some View { - VStack(alignment: .leading, spacing: 12) { - HStack(alignment: .firstTextBaseline) { - Text("Customize shortcuts for Lithe actions. Changes apply immediately.") - .font(LitheTheme.smallFont) - .foregroundStyle(LitheTheme.secondaryText) - Spacer() + HStack(spacing: 10) { + Text("macOS") + .font(.system(size: 12.5, weight: .medium)) + .frame(width: 190, height: 28, alignment: .leading) + .padding(.horizontal, 9) + .litheSettingsControlChrome() + Menu { Button("Restore All Defaults") { - editingTarget = nil - validationIssue = nil + cancelEditing() feature.resetAll() } - .buttonStyle(.bordered) - .lithePointer() + } label: { + Image(systemName: "gearshape") + .font(.system(size: 13)) } + .menuStyle(.borderlessButton) + .frame(width: 26) + .accessibilityLabel("Restore All Defaults") + .lithePointer() + Spacer() + } + .foregroundStyle(LitheTheme.primaryText) + .padding(.horizontal, 16) + .padding(.bottom, 12) + } + private var toolbar: some View { + HStack(spacing: 12) { + Button { + expandedGroups = Set(LitheActionGroup.allCases) + } label: { + Image(systemName: "arrow.down.right.and.arrow.up.left") + } + .help("Expand All") + + Button { + expandedGroups = [] + cancelEditing() + } label: { + Image(systemName: "arrow.up.left.and.arrow.down.right") + } + .help("Collapse All") + + Button { + if let selectedCommandID, + let command = LitheCommandCatalog.command(id: selectedCommandID) { + expandedGroups.insert(command.group) + beginEditing(commandID: selectedCommandID, bindingIndex: nil) + } + } label: { + Image(systemName: "pencil") + } + .disabled(selectedCommandID == nil) + .help("Add Shortcut") + + Spacer(minLength: 12) LitheSettingsSearchField("Search actions or shortcuts", text: $query) { _ in - editingTarget = nil - validationIssue = nil + cancelEditing() + selectedCommandID = nil } + .frame(width: 250) } - .foregroundStyle(LitheTheme.primaryText) + .font(.system(size: 12)) + .buttonStyle(.borderless) + .foregroundStyle(LitheTheme.secondaryText) .padding(.horizontal, 16) - .padding(.bottom, 16) + .frame(height: 38) + .background(alignment: .top) { Rectangle().fill(LitheTheme.divider).frame(height: 1) } + .background(alignment: .bottom) { Rectangle().fill(LitheTheme.divider).frame(height: 1) } } private func commandSection(_ section: KeyboardShortcutCommandSection) -> some View { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 8) { - Text(LocalizedStringKey(section.group.rawValue)) - .font(.system(size: 12, weight: .medium)) - .foregroundStyle(LitheTheme.secondaryText) - .fixedSize() - Rectangle().fill(LitheTheme.divider).frame(height: 1) + VStack(alignment: .leading, spacing: 0) { + Button { + if expandedGroups.contains(section.group) { + expandedGroups.remove(section.group) + cancelEditing() + } else { + expandedGroups.insert(section.group) + } + } label: { + HStack(spacing: 7) { + Image(systemName: "chevron.right") + .font(.system(size: 9, weight: .medium)) + .rotationEffect(.degrees(isExpanded(section.group) ? 90 : 0)) + .frame(width: 12) + Image(systemName: "folder") + .font(.system(size: 12)) + Text(LocalizedStringKey(section.group.rawValue)) + .font(.system(size: 12.5)) + Spacer() + } + .foregroundStyle(LitheTheme.primaryText) + .padding(.horizontal, 8) + .frame(height: 26) + .contentShape(Rectangle()) } - VStack(spacing: 0) { - ForEach(Array(section.commands.enumerated()), id: \.element.id) { index, command in + .buttonStyle(.plain) + .disabled(!query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + + if isExpanded(section.group) { + ForEach(section.commands) { command in commandRow(command) - if index < section.commands.count - 1 { - Rectangle().fill(LitheTheme.divider).frame(height: 1) - } } } - .background(LitheTheme.sidebar) - .clipShape(RoundedRectangle(cornerRadius: 6)) - .overlay { - RoundedRectangle(cornerRadius: 6) - .stroke(LitheTheme.divider, lineWidth: 1) - } } } + private func isExpanded(_ group: LitheActionGroup) -> Bool { + !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || expandedGroups.contains(group) + } + private func commandRow(_ command: LitheCommandDefinition) -> some View { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 12) { - VStack(alignment: .leading, spacing: 3) { - Text(LocalizedStringKey(command.title)) - .font(.system(size: 12.5, weight: .medium)) - Text(command.id) - .font(.system(size: 10.5, design: .monospaced)) - .foregroundStyle(LitheTheme.tertiaryText) + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 8) { + Button { + selectedCommandID = command.id + } label: { + HStack(spacing: 7) { + Image(systemName: "bolt.horizontal") + .font(.system(size: 11)) + .frame(width: 12) + Text(LocalizedStringKey(command.title)) + .font(.system(size: 12.5)) + .lineLimit(1) + Spacer(minLength: 6) + } + .contentShape(Rectangle()) } - .frame(minWidth: 160, maxWidth: .infinity, alignment: .leading) + .buttonStyle(.plain) shortcutControls(for: command) @@ -122,6 +196,12 @@ struct KeyboardShortcutSettingsView: View { .lithePointer() } } + .foregroundStyle(selectedCommandID == command.id ? LitheTheme.settingsSelectionText : LitheTheme.primaryText) + .padding(.leading, 31) + .padding(.trailing, 8) + .frame(height: 28) + .background(selectedCommandID == command.id ? LitheTheme.settingsSelection : .clear) + .clipShape(RoundedRectangle(cornerRadius: 4)) if editingTarget?.commandID == command.id { KeyboardShortcutRecorderView( @@ -134,6 +214,7 @@ struct KeyboardShortcutSettingsView: View { onCancel: cancelEditing ) .id("\(command.id)-\(editingTarget?.bindingIndex ?? -1)") + .padding(.leading, 31) if let validationIssue { Label { @@ -141,14 +222,12 @@ struct KeyboardShortcutSettingsView: View { } icon: { Image(systemName: "exclamationmark.triangle.fill") } - .font(.system(size: 11)) - .foregroundStyle(LitheTheme.warning) + .font(.system(size: 11)) + .foregroundStyle(LitheTheme.warning) + .padding(.leading, 31) } } } - .foregroundStyle(LitheTheme.primaryText) - .padding(.horizontal, 14) - .padding(.vertical, 11) } private func shortcutControls(for command: LitheCommandDefinition) -> some View { @@ -218,6 +297,7 @@ struct KeyboardShortcutSettingsView: View { } private func beginEditing(commandID: String, bindingIndex: Int?) { + selectedCommandID = commandID validationIssue = nil editingTarget = EditingTarget(commandID: commandID, bindingIndex: bindingIndex) } diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index d6b2afad7..9a082ac5f 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -1788,7 +1788,7 @@ "verificationStatus": "pending" }, "owner": "Settings", - "verification": "从设置 → 快捷键打开编辑器,按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。", + "verification": "macOS 从设置 → 快捷键打开分类树,检查初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、工具栏编辑、行内快捷键新增与删除及恢复默认;按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。", "notes": "Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。" }, { From 56fe036ee52f381ff19ae1e9bbd3a402ab00cdfb Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 12:44:09 +0800 Subject: [PATCH 06/19] style(macos): refine keymap settings tree controls --- docs/development/platform-parity-matrix.csv | 2 +- docs/development/platform-parity-matrix.md | 2 +- .../App/KeyboardShortcutSettingsView.swift | 133 +++++++++++------- shared/platform-feature-matrix.json | 2 +- 4 files changed, 83 insertions(+), 56 deletions(-) diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv index 6688aefd8..1f14e1645 100644 --- a/docs/development/platform-parity-matrix.csv +++ b/docs/development/platform-parity-matrix.csv @@ -70,7 +70,7 @@ references-pane,编辑器,代码结构,引用结果面板与引用跳转,已实 diagnostics-workspace,可观测性,诊断与日志,工作区诊断与问题面板,已实现,待验证,已实现,待验证,Diagnostics,制造多个来源的错误和警告,验证筛选、排序、定位和清除。,,macos/Sources/Lithe/Application/Features/DiagnosticsFeatureModel.swift; macos/Sources/Lithe/Views/Language/JavaProblemsView.swift,windows/tauri/src/features/diagnostics; windows/tauri/src/features/logging; windows/tauri/src/ui/dialog.tsx diagnostics-export,可观测性,诊断与日志,诊断包导出与脱敏,已实现,待验证,已实现,待验证,Diagnostics,导出诊断包,确认敏感路径、凭据和大文件被正确脱敏或限制;检查预览说明和文件列表在确认弹窗内完整显示、长内容正常换行且不溢出。,,macos/Sources/Lithe/Views/Diagnostics/DiagnosticsExportSheet.swift; macos/Sources/Lithe/Services/Diagnostics,windows/tauri/src/features/diagnostics application-settings,设置,应用配置,应用、项目与运行设置,已实现,待验证,已实现,待验证,Settings,修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift; macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift,windows/tauri/src/features/settings -keymap-customization,设置,应用配置,快捷键查看与自定义,已实现,待验证,已实现,待验证,Settings,macOS 从设置 → 快捷键打开分类树,检查初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、工具栏编辑、行内快捷键新增与删除及恢复默认;按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。,Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。,macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift; macos/Sources/Lithe/Models/Keymap,windows/tauri/src/features/keymaps; windows/tauri/src/features/settings/components/macos-settings-panels.tsx; windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx; windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx +keymap-customization,设置,应用配置,快捷键查看与自定义,已实现,待验证,已实现,待验证,Settings,macOS 从设置 → 快捷键打开分类树,检查初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、右侧紧凑键帽及工具栏编辑菜单;通过键帽点击编辑,通过工具栏或右键菜单新增、删除与恢复默认,并检查键盘辅助功能能访问这些操作;按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。,Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。,macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift; macos/Sources/Lithe/Models/Keymap,windows/tauri/src/features/keymaps; windows/tauri/src/features/settings/components/macos-settings-panels.tsx; windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx; windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx plugin-extension-management,扩展,插件与主题,插件、语言服务与主题管理,已实现,待验证,已实现,待验证,Extensions,启用、禁用和恢复插件/主题,确认启动加载、失败隔离和设置持久化。,,macos/Sources/Lithe/Views/App/PluginManagementView.swift; macos/Sources/Lithe/Platform/MacOS/Plugins,windows/tauri/src/extensions; windows/tauri/src/features/settings onboarding-welcome,启动与窗口,首次启动,欢迎页、项目入口与首次启动引导,已实现,待验证,已实现,待验证,Onboarding,清空首次启动状态,验证打开项目、克隆项目、最近项目和跳过引导。,,macos/Sources/Lithe/Views/App/WelcomeView.swift; macos/Sources/Lithe/Views/Workspace,windows/tauri/src/features/onboarding; windows/tauri/src/features/bootstrap command-palette,工作台,命令与布局,命令面板与全局动作,已实现,待验证,已实现,待验证,Workbench,搜索并执行打开面板、运行、Git 和设置命令,确认快捷键和不可用命令状态。,,macos/Sources/Lithe/Models/Keymap/LitheCommandCatalog.swift; macos/Sources/Lithe/Views/Search/SearchEverywhereView.swift,windows/tauri/src/features/command-palette diff --git a/docs/development/platform-parity-matrix.md b/docs/development/platform-parity-matrix.md index e1666592c..c37d64350 100644 --- a/docs/development/platform-parity-matrix.md +++ b/docs/development/platform-parity-matrix.md @@ -205,7 +205,7 @@ | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | | 应用配置 | **应用、项目与运行设置**
application-settings | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift`、`macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift`、`macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings` | Settings | 修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。 | | -| 应用配置 | **快捷键查看与自定义**
keymap-customization | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift`、`macos/Sources/Lithe/Models/Keymap` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/keymaps`、`windows/tauri/src/features/settings/components/macos-settings-panels.tsx`、`windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx`、`windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx` | Settings | macOS 从设置 → 快捷键打开分类树,检查初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、工具栏编辑、行内快捷键新增与删除及恢复默认;按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。 | Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。 | +| 应用配置 | **快捷键查看与自定义**
keymap-customization | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift`、`macos/Sources/Lithe/Models/Keymap` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/keymaps`、`windows/tauri/src/features/settings/components/macos-settings-panels.tsx`、`windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx`、`windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx` | Settings | macOS 从设置 → 快捷键打开分类树,检查初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、右侧紧凑键帽及工具栏编辑菜单;通过键帽点击编辑,通过工具栏或右键菜单新增、删除与恢复默认,并检查键盘辅助功能能访问这些操作;按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。 | Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。 | diff --git a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift index f0bbd6673..889f5c163 100644 --- a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift @@ -35,7 +35,7 @@ struct KeyboardShortcutSettingsView: View { } } .padding(.horizontal, 12) - .padding(.vertical, 6) + .padding(.vertical, 4) } .litheScrollViewChrome(alwaysShowVertical: true, usesCompactScrollers: true) } @@ -67,8 +67,10 @@ struct KeyboardShortcutSettingsView: View { } label: { Image(systemName: "gearshape") .font(.system(size: 13)) + .foregroundStyle(LitheTheme.secondaryText) } .menuStyle(.borderlessButton) + .tint(LitheTheme.secondaryText) .frame(width: 26) .accessibilityLabel("Restore All Defaults") .lithePointer() @@ -80,11 +82,11 @@ struct KeyboardShortcutSettingsView: View { } private var toolbar: some View { - HStack(spacing: 12) { + HStack(spacing: 14) { Button { expandedGroups = Set(LitheActionGroup.allCases) } label: { - Image(systemName: "arrow.down.right.and.arrow.up.left") + expansionIcon(expand: true) } .help("Expand All") @@ -92,21 +94,36 @@ struct KeyboardShortcutSettingsView: View { expandedGroups = [] cancelEditing() } label: { - Image(systemName: "arrow.up.left.and.arrow.down.right") + expansionIcon(expand: false) } .help("Collapse All") - Button { + Menu { if let selectedCommandID, let command = LitheCommandCatalog.command(id: selectedCommandID) { - expandedGroups.insert(command.group) - beginEditing(commandID: selectedCommandID, bindingIndex: nil) + Button("Add Shortcut") { + expandedGroups.insert(command.group) + beginEditing(commandID: selectedCommandID, bindingIndex: nil) + } + ForEach(Array(feature.effectiveBindings(for: selectedCommandID).enumerated()), id: \.offset) { index, binding in + Button("Remove \(binding.displayText)") { + removeBinding(commandID: selectedCommandID, index: index) + } + } + if feature.isCustomized(selectedCommandID) { + Button("Restore Default") { + cancelEditing() + feature.resetCommand(selectedCommandID) + } + } } } label: { Image(systemName: "pencil") } + .menuStyle(.borderlessButton) + .tint(LitheTheme.secondaryText) .disabled(selectedCommandID == nil) - .help("Add Shortcut") + .help("Edit Shortcuts") Spacer(minLength: 12) LitheSettingsSearchField("Search actions or shortcuts", text: $query) { _ in @@ -119,11 +136,20 @@ struct KeyboardShortcutSettingsView: View { .buttonStyle(.borderless) .foregroundStyle(LitheTheme.secondaryText) .padding(.horizontal, 16) - .frame(height: 38) + .frame(height: 36) .background(alignment: .top) { Rectangle().fill(LitheTheme.divider).frame(height: 1) } .background(alignment: .bottom) { Rectangle().fill(LitheTheme.divider).frame(height: 1) } } + private func expansionIcon(expand: Bool) -> some View { + VStack(spacing: 3) { + Image(systemName: expand ? "chevron.up" : "chevron.down") + Image(systemName: expand ? "chevron.down" : "chevron.up") + } + .font(.system(size: 7, weight: .medium)) + .frame(width: 16, height: 20) + } + private func commandSection(_ section: KeyboardShortcutCommandSection) -> some View { VStack(alignment: .leading, spacing: 0) { Button { @@ -147,7 +173,7 @@ struct KeyboardShortcutSettingsView: View { } .foregroundStyle(LitheTheme.primaryText) .padding(.horizontal, 8) - .frame(height: 26) + .frame(height: 24) .contentShape(Rectangle()) } .buttonStyle(.plain) @@ -171,37 +197,35 @@ struct KeyboardShortcutSettingsView: View { Button { selectedCommandID = command.id } label: { - HStack(spacing: 7) { - Image(systemName: "bolt.horizontal") - .font(.system(size: 11)) - .frame(width: 12) + HStack(spacing: 0) { Text(LocalizedStringKey(command.title)) .font(.system(size: 12.5)) .lineLimit(1) - Spacer(minLength: 6) + Spacer(minLength: 8) } .contentShape(Rectangle()) } .buttonStyle(.plain) shortcutControls(for: command) - + } + .foregroundStyle(selectedCommandID == command.id ? LitheTheme.settingsSelectionText : LitheTheme.primaryText) + .padding(.leading, 36) + .padding(.trailing, 6) + .frame(height: 24) + .background(selectedCommandID == command.id ? LitheTheme.settingsSelection : .clear) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .contextMenu { + Button("Add Shortcut") { + beginEditing(commandID: command.id, bindingIndex: nil) + } if feature.isCustomized(command.id) { - Button("Reset") { + Button("Restore Default") { cancelEditing() feature.resetCommand(command.id) } - .buttonStyle(.borderless) - .font(.system(size: 11.5)) - .lithePointer() } } - .foregroundStyle(selectedCommandID == command.id ? LitheTheme.settingsSelectionText : LitheTheme.primaryText) - .padding(.leading, 31) - .padding(.trailing, 8) - .frame(height: 28) - .background(selectedCommandID == command.id ? LitheTheme.settingsSelection : .clear) - .clipShape(RoundedRectangle(cornerRadius: 4)) if editingTarget?.commandID == command.id { KeyboardShortcutRecorderView( @@ -236,6 +260,7 @@ struct KeyboardShortcutSettingsView: View { if bindings.isEmpty { Button("Not Assigned") { beginEditing(commandID: command.id, bindingIndex: nil) } .buttonStyle(.borderless) + .font(.system(size: 12)) .foregroundStyle(LitheTheme.tertiaryText) .lithePointer() } else { @@ -243,16 +268,6 @@ struct KeyboardShortcutSettingsView: View { bindingChip(binding, commandID: command.id, index: index) } } - - Button { - beginEditing(commandID: command.id, bindingIndex: nil) - } label: { - Image(systemName: "plus") - .font(.system(size: 10, weight: .semibold)) - } - .buttonStyle(.borderless) - .help("Add Shortcut") - .lithePointer() } } @@ -261,27 +276,39 @@ struct KeyboardShortcutSettingsView: View { commandID: String, index: Int ) -> some View { - HStack(spacing: 3) { - Button(binding.displayText) { - beginEditing(commandID: commandID, bindingIndex: index) + Button { + beginEditing(commandID: commandID, bindingIndex: index) + } label: { + HStack(spacing: 2) { + ForEach(Array(keycapLabels(for: binding).enumerated()), id: \.offset) { _, label in + Text(label) + .font(.system(size: 11, weight: .medium, design: .rounded)) + .frame(minWidth: 14, minHeight: 17) + .padding(.horizontal, 2) + .litheSettingsControlChrome(cornerRadius: 3) + } } - .buttonStyle(.borderless) - .font(.system(size: 11.5, weight: .medium, design: .rounded)) - .lithePointer() - - Button { + } + .buttonStyle(.plain) + .lithePointer() + .help("Edit Shortcut") + .accessibilityLabel(binding.displayText) + .contextMenu { + Button("Remove Shortcut") { removeBinding(commandID: commandID, index: index) - } label: { - Image(systemName: "xmark") - .font(.system(size: 8, weight: .bold)) } - .buttonStyle(.borderless) - .help("Remove") - .lithePointer() } - .padding(.horizontal, 7) - .frame(height: 24) - .litheSettingsControlChrome() + } + + private func keycapLabels(for binding: KeyboardShortcutBinding) -> [String] { + guard let (_, modifiers) = binding.keyPressValue else { return [binding.displayText] } + let symbols = [ + modifiers.contains(.control) ? "⌃" : nil, + modifiers.contains(.option) ? "⌥" : nil, + modifiers.contains(.shift) ? "⇧" : nil, + modifiers.contains(.command) ? "⌘" : nil + ].compactMap { $0 } + return symbols + [String(binding.displayText.dropFirst(symbols.count))] } private var emptyState: some View { diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index 9a082ac5f..e7032b2bb 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -1788,7 +1788,7 @@ "verificationStatus": "pending" }, "owner": "Settings", - "verification": "macOS 从设置 → 快捷键打开分类树,检查初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、工具栏编辑、行内快捷键新增与删除及恢复默认;按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。", + "verification": "macOS 从设置 → 快捷键打开分类树,检查初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、右侧紧凑键帽及工具栏编辑菜单;通过键帽点击编辑,通过工具栏或右键菜单新增、删除与恢复默认,并检查键盘辅助功能能访问这些操作;按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。", "notes": "Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。" }, { From 376d9a23a1d5e6d18533e8a4bc1c5c525916a664 Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 12:52:31 +0800 Subject: [PATCH 07/19] style(macos): align keymap toolbar icons and search field with IDEA --- .../IDEAIcons/expui/general/collapseAll.svg | 5 +++++ .../expui/general/collapseAll_dark.svg | 5 +++++ .../IDEAIcons/expui/general/edit.svg | 4 ++++ .../IDEAIcons/expui/general/edit_dark.svg | 4 ++++ .../IDEAIcons/expui/general/expandAll.svg | 5 +++++ .../expui/general/expandAll_dark.svg | 5 +++++ .../App/KeyboardShortcutSettingsView.swift | 22 ++++++------------- .../Components/LitheSettingsControls.swift | 3 +-- 8 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 macos/Resources/IDEAIcons/expui/general/collapseAll.svg create mode 100644 macos/Resources/IDEAIcons/expui/general/collapseAll_dark.svg create mode 100644 macos/Resources/IDEAIcons/expui/general/edit.svg create mode 100644 macos/Resources/IDEAIcons/expui/general/edit_dark.svg create mode 100644 macos/Resources/IDEAIcons/expui/general/expandAll.svg create mode 100644 macos/Resources/IDEAIcons/expui/general/expandAll_dark.svg diff --git a/macos/Resources/IDEAIcons/expui/general/collapseAll.svg b/macos/Resources/IDEAIcons/expui/general/collapseAll.svg new file mode 100644 index 000000000..f5463c66f --- /dev/null +++ b/macos/Resources/IDEAIcons/expui/general/collapseAll.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/macos/Resources/IDEAIcons/expui/general/collapseAll_dark.svg b/macos/Resources/IDEAIcons/expui/general/collapseAll_dark.svg new file mode 100644 index 000000000..c66426bb7 --- /dev/null +++ b/macos/Resources/IDEAIcons/expui/general/collapseAll_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/macos/Resources/IDEAIcons/expui/general/edit.svg b/macos/Resources/IDEAIcons/expui/general/edit.svg new file mode 100644 index 000000000..bd3c0a391 --- /dev/null +++ b/macos/Resources/IDEAIcons/expui/general/edit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/macos/Resources/IDEAIcons/expui/general/edit_dark.svg b/macos/Resources/IDEAIcons/expui/general/edit_dark.svg new file mode 100644 index 000000000..e75126d05 --- /dev/null +++ b/macos/Resources/IDEAIcons/expui/general/edit_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/macos/Resources/IDEAIcons/expui/general/expandAll.svg b/macos/Resources/IDEAIcons/expui/general/expandAll.svg new file mode 100644 index 000000000..71ccb3cfb --- /dev/null +++ b/macos/Resources/IDEAIcons/expui/general/expandAll.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/macos/Resources/IDEAIcons/expui/general/expandAll_dark.svg b/macos/Resources/IDEAIcons/expui/general/expandAll_dark.svg new file mode 100644 index 000000000..51e066ac1 --- /dev/null +++ b/macos/Resources/IDEAIcons/expui/general/expandAll_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift index 889f5c163..a353e065e 100644 --- a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift @@ -82,11 +82,11 @@ struct KeyboardShortcutSettingsView: View { } private var toolbar: some View { - HStack(spacing: 14) { + HStack(spacing: 8) { Button { expandedGroups = Set(LitheActionGroup.allCases) } label: { - expansionIcon(expand: true) + LitheIDEAIcon(resourcePath: "expui/general/expandAll.svg", size: 16, fallbackSystemImage: "arrow.down.right.and.arrow.up.left") } .help("Expand All") @@ -94,7 +94,7 @@ struct KeyboardShortcutSettingsView: View { expandedGroups = [] cancelEditing() } label: { - expansionIcon(expand: false) + LitheIDEAIcon(resourcePath: "expui/general/collapseAll.svg", size: 16, fallbackSystemImage: "arrow.up.left.and.arrow.down.right") } .help("Collapse All") @@ -118,7 +118,7 @@ struct KeyboardShortcutSettingsView: View { } } } label: { - Image(systemName: "pencil") + LitheIDEAIcon(resourcePath: "expui/general/edit.svg", size: 16, fallbackSystemImage: "pencil") } .menuStyle(.borderlessButton) .tint(LitheTheme.secondaryText) @@ -126,11 +126,12 @@ struct KeyboardShortcutSettingsView: View { .help("Edit Shortcuts") Spacer(minLength: 12) - LitheSettingsSearchField("Search actions or shortcuts", text: $query) { _ in + LitheSettingsSearchField("", text: $query) { _ in cancelEditing() selectedCommandID = nil } - .frame(width: 250) + .frame(width: 244) + .accessibilityLabel("Search actions or shortcuts") } .font(.system(size: 12)) .buttonStyle(.borderless) @@ -141,15 +142,6 @@ struct KeyboardShortcutSettingsView: View { .background(alignment: .bottom) { Rectangle().fill(LitheTheme.divider).frame(height: 1) } } - private func expansionIcon(expand: Bool) -> some View { - VStack(spacing: 3) { - Image(systemName: expand ? "chevron.up" : "chevron.down") - Image(systemName: expand ? "chevron.down" : "chevron.up") - } - .font(.system(size: 7, weight: .medium)) - .frame(width: 16, height: 20) - } - private func commandSection(_ section: KeyboardShortcutCommandSection) -> some View { VStack(alignment: .leading, spacing: 0) { Button { diff --git a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift index 7ea33bed2..82a6b11b2 100644 --- a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift +++ b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift @@ -48,8 +48,7 @@ struct LitheSettingsSearchField: View { var body: some View { HStack(spacing: 7) { - Image(systemName: "magnifyingglass") - .font(.system(size: 12, weight: .medium)) + LitheSystemIcon(systemImage: "magnifyingglass", size: 14) .foregroundStyle(LitheTheme.tertiaryText) TextField(placeholder, text: $text) From bbbde425451848fa6593928492d62556e4c1cde5 Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 12:59:21 +0800 Subject: [PATCH 08/19] style(macos): preserve IntelliJ keymap icon colors and tree glyphs --- .../IDEAIcons/expui/general/chevronRight.svg | 4 ++++ .../expui/general/chevronRight_dark.svg | 4 ++++ .../Resources/IDEAIcons/expui/general/search.svg | 5 +++++ .../IDEAIcons/expui/general/search_dark.svg | 5 +++++ .../Views/App/KeyboardShortcutSettingsView.swift | 16 ++++++---------- .../Views/Components/LitheSettingsControls.swift | 3 +-- 6 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 macos/Resources/IDEAIcons/expui/general/chevronRight.svg create mode 100644 macos/Resources/IDEAIcons/expui/general/chevronRight_dark.svg create mode 100644 macos/Resources/IDEAIcons/expui/general/search.svg create mode 100644 macos/Resources/IDEAIcons/expui/general/search_dark.svg diff --git a/macos/Resources/IDEAIcons/expui/general/chevronRight.svg b/macos/Resources/IDEAIcons/expui/general/chevronRight.svg new file mode 100644 index 000000000..5120ed891 --- /dev/null +++ b/macos/Resources/IDEAIcons/expui/general/chevronRight.svg @@ -0,0 +1,4 @@ + + + + diff --git a/macos/Resources/IDEAIcons/expui/general/chevronRight_dark.svg b/macos/Resources/IDEAIcons/expui/general/chevronRight_dark.svg new file mode 100644 index 000000000..838142106 --- /dev/null +++ b/macos/Resources/IDEAIcons/expui/general/chevronRight_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/macos/Resources/IDEAIcons/expui/general/search.svg b/macos/Resources/IDEAIcons/expui/general/search.svg new file mode 100644 index 000000000..021b0c394 --- /dev/null +++ b/macos/Resources/IDEAIcons/expui/general/search.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/macos/Resources/IDEAIcons/expui/general/search_dark.svg b/macos/Resources/IDEAIcons/expui/general/search_dark.svg new file mode 100644 index 000000000..d5a82d9c1 --- /dev/null +++ b/macos/Resources/IDEAIcons/expui/general/search_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift index a353e065e..d017b8c68 100644 --- a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift @@ -65,8 +65,7 @@ struct KeyboardShortcutSettingsView: View { feature.resetAll() } } label: { - Image(systemName: "gearshape") - .font(.system(size: 13)) + LitheSystemIcon(systemImage: "gearshape", size: 16) .foregroundStyle(LitheTheme.secondaryText) } .menuStyle(.borderlessButton) @@ -86,7 +85,7 @@ struct KeyboardShortcutSettingsView: View { Button { expandedGroups = Set(LitheActionGroup.allCases) } label: { - LitheIDEAIcon(resourcePath: "expui/general/expandAll.svg", size: 16, fallbackSystemImage: "arrow.down.right.and.arrow.up.left") + LitheIDEAIcon(resourcePath: "expui/general/expandAll.svg", size: 16, fallbackSystemImage: "arrow.down.right.and.arrow.up.left", preservesOriginalColors: true) } .help("Expand All") @@ -94,7 +93,7 @@ struct KeyboardShortcutSettingsView: View { expandedGroups = [] cancelEditing() } label: { - LitheIDEAIcon(resourcePath: "expui/general/collapseAll.svg", size: 16, fallbackSystemImage: "arrow.up.left.and.arrow.down.right") + LitheIDEAIcon(resourcePath: "expui/general/collapseAll.svg", size: 16, fallbackSystemImage: "arrow.up.left.and.arrow.down.right", preservesOriginalColors: true) } .help("Collapse All") @@ -118,7 +117,7 @@ struct KeyboardShortcutSettingsView: View { } } } label: { - LitheIDEAIcon(resourcePath: "expui/general/edit.svg", size: 16, fallbackSystemImage: "pencil") + LitheIDEAIcon(resourcePath: "expui/general/edit.svg", size: 16, fallbackSystemImage: "pencil", preservesOriginalColors: true) } .menuStyle(.borderlessButton) .tint(LitheTheme.secondaryText) @@ -153,12 +152,9 @@ struct KeyboardShortcutSettingsView: View { } } label: { HStack(spacing: 7) { - Image(systemName: "chevron.right") - .font(.system(size: 9, weight: .medium)) + LitheIDEAIcon(resourcePath: "expui/general/chevronRight.svg", size: 16, fallbackSystemImage: "chevron.right", preservesOriginalColors: true) .rotationEffect(.degrees(isExpanded(section.group) ? 90 : 0)) - .frame(width: 12) - Image(systemName: "folder") - .font(.system(size: 12)) + LitheIcon(kind: .folder, size: 16) Text(LocalizedStringKey(section.group.rawValue)) .font(.system(size: 12.5)) Spacer() diff --git a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift index 82a6b11b2..1ecb6dbc9 100644 --- a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift +++ b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift @@ -48,8 +48,7 @@ struct LitheSettingsSearchField: View { var body: some View { HStack(spacing: 7) { - LitheSystemIcon(systemImage: "magnifyingglass", size: 14) - .foregroundStyle(LitheTheme.tertiaryText) + LitheIDEAIcon(resourcePath: "expui/general/search.svg", size: 16, fallbackSystemImage: "magnifyingglass", preservesOriginalColors: true) TextField(placeholder, text: $text) .textFieldStyle(.plain) From d4a4038d53e0b3d22ed70456b249aaf018fb825d Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 13:12:14 +0800 Subject: [PATCH 09/19] style(macos): remove pressed dimming from borderless buttons --- macos/Sources/Lithe/Theme/LitheTheme.swift | 8 +++- .../Lithe/Views/Agent/AgentComposerView.swift | 4 +- .../Views/Agent/AgentConversationView.swift | 2 +- .../Views/Agent/AgentFileReferenceList.swift | 2 +- .../Lithe/Views/Agent/AgentHistoryView.swift | 14 +++---- .../Views/Agent/AgentPanelSettingsView.swift | 8 ++-- .../Views/Agent/AgentSessionSelectors.swift | 8 ++-- .../Views/Agent/AgentTranscriptView.swift | 10 ++--- .../App/KeyboardShortcutSettingsView.swift | 10 ++--- .../Views/App/PluginManagementView.swift | 4 +- .../App/ProjectRuntimeSettingsView.swift | 2 +- .../App/RunConfigurationSettingsView.swift | 2 +- .../Lithe/Views/App/SettingsView.swift | 14 +++---- .../Views/App/StableRollbackControl.swift | 2 +- .../Lithe/Views/App/UpdateControl.swift | 4 +- .../Sources/Lithe/Views/App/WelcomeView.swift | 4 +- .../Views/Components/LitheContextMenu.swift | 2 +- .../Components/LitheSettingsControls.swift | 12 +++--- .../Database/DatabaseSQLWorkspaceView.swift | 16 ++++---- .../Views/Database/DatabaseSidebarView.swift | 16 ++++---- .../DatabaseSpecializedWorkspaceViews.swift | 8 ++-- .../Views/Database/DatabaseTableView.swift | 40 +++++++++---------- .../Lithe/Views/Debug/GenericDebugView.swift | 38 +++++++++--------- .../Views/Diff/DiffCollapsedBandView.swift | 2 +- .../Lithe/Views/Diff/DiffReviewView.swift | 4 +- .../Lithe/Views/Editor/EditorAreaView.swift | 10 ++--- .../Lithe/Views/Editor/HTMLPreviewView.swift | 2 +- .../Views/Editor/MarkdownPreviewView.swift | 2 +- .../Views/Git/BranchComparisonView.swift | 2 +- .../Views/Git/BranchSwitcherPopover.swift | 8 ++-- .../Lithe/Views/Git/ChangesSidebarView.swift | 20 +++++----- .../Views/Git/GitCommitDiffReviewView.swift | 2 +- .../Lithe/Views/Git/GitConsoleEntryView.swift | 2 +- .../Lithe/Views/Git/GitConsoleView.swift | 2 +- .../Lithe/Views/Git/GitGraphView.swift | 4 +- .../Views/Git/GitHistoryRewriteView.swift | 2 +- .../Git/GitInteractiveRebaseStatusView.swift | 2 +- .../Lithe/Views/Git/GitLogFilterPopover.swift | 4 +- .../Sources/Lithe/Views/Git/GitLogView.swift | 30 +++++++------- .../Lithe/Views/Git/GitPatchView.swift | 4 +- .../Lithe/Views/Git/GitWorktreesView.swift | 10 ++--- .../Views/GitHub/GitHubPullRequestsView.swift | 14 +++---- .../Views/History/LocalHistoryView.swift | 2 +- .../History/ProjectLocalHistoryView.swift | 2 +- .../Views/Language/JavaProblemsView.swift | 2 +- .../Views/Language/JavaReferencesView.swift | 4 +- .../Language/LanguageServerSetupView.swift | 2 +- .../Views/Language/LanguageTestsView.swift | 6 +-- .../Run/JavaRunConfigurationEditorView.swift | 4 +- .../Views/Run/MavenBuildOutputView.swift | 2 +- macos/Sources/Lithe/Views/Run/MavenView.swift | 20 +++++----- macos/Sources/Lithe/Views/Run/RunView.swift | 14 +++---- .../Lithe/Views/Run/SpringEndpointsView.swift | 2 +- .../Views/Search/ProjectReplaceView.swift | 4 +- .../ProjectReplacementSourcePreview.swift | 2 +- .../Views/Search/SearchEverywhereView.swift | 8 ++-- .../Views/Search/SearchSidebarView.swift | 2 +- .../Lithe/Views/Terminal/TerminalView.swift | 2 +- .../Views/Workbench/OutputTextView.swift | 4 +- .../ProjectPreparationStatusView.swift | 2 +- .../Workbench/WorkbenchStatusViews.swift | 4 +- .../Lithe/Views/Workbench/WorkbenchView.swift | 34 ++++++++-------- .../Workspace/DependencySidebarView.swift | 20 +++++----- .../Views/Workspace/ProjectSidebarView.swift | 4 +- .../Workspace/ProjectSwitcherPopover.swift | 6 +-- 65 files changed, 256 insertions(+), 252 deletions(-) diff --git a/macos/Sources/Lithe/Theme/LitheTheme.swift b/macos/Sources/Lithe/Theme/LitheTheme.swift index c15b04ff1..975408435 100644 --- a/macos/Sources/Lithe/Theme/LitheTheme.swift +++ b/macos/Sources/Lithe/Theme/LitheTheme.swift @@ -539,13 +539,17 @@ struct LitheIconButtonStyle: ButtonStyle { } } -/// Keeps file-tree rows visually stable while they are being activated. -struct LitheTreeRowButtonStyle: ButtonStyle { +/// Keeps borderless button labels at full opacity while pressed. +struct LitheNoPressButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label } } +extension ButtonStyle where Self == LitheNoPressButtonStyle { + static var litheNoPress: LitheNoPressButtonStyle { .init() } +} + private struct LitheRowHoverModifier: ViewModifier { let isActive: Bool let cornerRadius: CGFloat diff --git a/macos/Sources/Lithe/Views/Agent/AgentComposerView.swift b/macos/Sources/Lithe/Views/Agent/AgentComposerView.swift index d3fc09dbe..1cbdf5ad0 100644 --- a/macos/Sources/Lithe/Views/Agent/AgentComposerView.swift +++ b/macos/Sources/Lithe/Views/Agent/AgentComposerView.swift @@ -83,7 +83,7 @@ struct AgentComposerView: View { Button { showsFilePicker = true } label: { Label(isDropTargeted ? "Drop files here" : "Attach files", systemImage: "paperclip") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help("Drag files here or click to choose files") Spacer(minLength: 0) } @@ -129,7 +129,7 @@ struct AgentComposerView: View { .frame(width: 26, height: 26) .background(AgentPanelStyle.context, in: RoundedRectangle(cornerRadius: 4)) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .disabled(isCancelling || (!isResponding && (!hasContent || isBlocked || isConfiguring))) .help(isCancelling ? "Stopping…" : (isResponding ? "Stop" : "Send")) diff --git a/macos/Sources/Lithe/Views/Agent/AgentConversationView.swift b/macos/Sources/Lithe/Views/Agent/AgentConversationView.swift index 142168d74..b17b7a57c 100644 --- a/macos/Sources/Lithe/Views/Agent/AgentConversationView.swift +++ b/macos/Sources/Lithe/Views/Agent/AgentConversationView.swift @@ -375,7 +375,7 @@ private struct AgentSessionTab: View { .font(.system(size: 9, weight: .bold)) .frame(width: 14, height: 14) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .foregroundStyle(LitheTheme.tertiaryText) .help("Close conversation") diff --git a/macos/Sources/Lithe/Views/Agent/AgentFileReferenceList.swift b/macos/Sources/Lithe/Views/Agent/AgentFileReferenceList.swift index 4eaa5e1f9..950a8f921 100644 --- a/macos/Sources/Lithe/Views/Agent/AgentFileReferenceList.swift +++ b/macos/Sources/Lithe/Views/Agent/AgentFileReferenceList.swift @@ -15,7 +15,7 @@ struct AgentFileReferenceList: View { Button { onRemove(file.id) } label: { Image(systemName: "xmark").font(.system(size: 9)) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .accessibilityLabel(String(format: String(localized: "Remove file %@"), file.name)) } .font(.system(size: 11)) diff --git a/macos/Sources/Lithe/Views/Agent/AgentHistoryView.swift b/macos/Sources/Lithe/Views/Agent/AgentHistoryView.swift index 7e1ec1549..0eb5e6d4c 100644 --- a/macos/Sources/Lithe/Views/Agent/AgentHistoryView.swift +++ b/macos/Sources/Lithe/Views/Agent/AgentHistoryView.swift @@ -72,7 +72,7 @@ struct AgentHistoryView: View { VStack(spacing: 0) { HStack { Button(action: onBack) { Label("Back", systemImage: "arrow.left") } - .buttonStyle(.plain).lithePointer() + .buttonStyle(.litheNoPress).lithePointer() .accessibilityIdentifier("agent-history-back") Spacer() Text("Conversation history").fontWeight(.medium) @@ -98,7 +98,7 @@ struct AgentHistoryView: View { ProgressView().controlSize(.mini) Text("Exporting conversations…") Spacer() - Button("Cancel") { history.cancelExport() }.buttonStyle(.plain) + Button("Cancel") { history.cancelExport() }.buttonStyle(.litheNoPress) }.font(.system(size: 12)).padding(12) } if sessions.isEmpty { @@ -161,7 +161,7 @@ struct AgentHistoryView: View { } } label: { Text("Manage selected") } .disabled(selected.isEmpty) - }.font(.system(size: 11)).buttonStyle(.borderless) + }.font(.system(size: 11)).buttonStyle(.litheNoPress) } if filter == .removed { Text("Removed conversations are hidden only in Lithe. The Agent's original records are kept.") @@ -204,7 +204,7 @@ struct AgentHistoryView: View { .accessibilityIdentifier("agent-history-search") if !query.isEmpty { Button { query = "" } label: { Image(systemName: "xmark") } - .buttonStyle(.plain).help("Clear search") + .buttonStyle(.litheNoPress).help("Clear search") } else { Image(systemName: "magnifyingglass") } } .font(.system(size: 12)) @@ -302,7 +302,7 @@ private struct AgentHistoryRow: View { if isSelecting { Button(action: onToggleSelection) { Image(systemName: isSelected ? "checkmark.square.fill" : "square") - }.buttonStyle(.plain).help("Select conversation") + }.buttonStyle(.litheNoPress).help("Select conversation") } AgentBrandIcon(name: agentName, size: 18).foregroundStyle(AgentPanelStyle.text) if isEditing { @@ -318,7 +318,7 @@ private struct AgentHistoryRow: View { Text(title).font(.system(size: 14, weight: .semibold)) .lineLimit(1).frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) - }.buttonStyle(.plain).lithePointer() + }.buttonStyle(.litheNoPress).lithePointer() .disabled(!isSelecting && !canOpen).help(title) if let date = AgentHistoryPresentation.date(session.updatedAt) { Text(date, format: .relative(presentation: .named, unitsStyle: .abbreviated)) @@ -365,6 +365,6 @@ private struct AgentHistoryRow: View { private func icon(_ name: String, _ title: LocalizedStringKey, _ action: @escaping () -> Void) -> some View { Button(action: action) { Image(systemName: name).font(.system(size: 11)).frame(width: 22, height: 22) } - .buttonStyle(.plain).lithePointer().help(title).accessibilityLabel(title) + .buttonStyle(.litheNoPress).lithePointer().help(title).accessibilityLabel(title) } } diff --git a/macos/Sources/Lithe/Views/Agent/AgentPanelSettingsView.swift b/macos/Sources/Lithe/Views/Agent/AgentPanelSettingsView.swift index 33f2d1969..6f51bf7e8 100644 --- a/macos/Sources/Lithe/Views/Agent/AgentPanelSettingsView.swift +++ b/macos/Sources/Lithe/Views/Agent/AgentPanelSettingsView.swift @@ -94,7 +94,7 @@ struct AgentPanelSettingsView: View { ) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help(item.title) } @@ -207,7 +207,7 @@ private struct AgentsManagementPage: View { .frame(width: 30, height: 30) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .foregroundStyle(LitheTheme.secondaryText) .disabled(feature.phase == .checking) @@ -240,7 +240,7 @@ private struct AgentsManagementPage: View { ) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } @@ -407,7 +407,7 @@ private struct AgentDetailView: View { } .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() if isExpanded { HStack(alignment: .top, spacing: 8) { diff --git a/macos/Sources/Lithe/Views/Agent/AgentSessionSelectors.swift b/macos/Sources/Lithe/Views/Agent/AgentSessionSelectors.swift index 19e94c632..e26f085db 100644 --- a/macos/Sources/Lithe/Views/Agent/AgentSessionSelectors.swift +++ b/macos/Sources/Lithe/Views/Agent/AgentSessionSelectors.swift @@ -70,7 +70,7 @@ struct AgentSessionSelectors: View { Text(AgentSessionSelectorPresentation.currentTitle(mode)) } } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help(AgentSessionSelectorPresentation.localized(mode.name)) .accessibilityLabel(Text("Approval mode")) .accessibilityValue(AgentSessionSelectorPresentation.currentTitle(mode)) @@ -86,7 +86,7 @@ struct AgentSessionSelectors: View { Text(modelSummary(model)) } } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help(model.currentLabel) .accessibilityLabel(Text("Model")) .accessibilityValue(model.currentLabel) @@ -253,7 +253,7 @@ private struct AgentModelSettingRow: View { .contentShape(Rectangle()) .litheRowHover() } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } } @@ -313,7 +313,7 @@ private struct AgentSelectorRow: View { .background(isSelected ? AgentPanelStyle.selected : (isHovering ? AgentPanelStyle.context : .clear)) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .onHover { isHovering = $0 } .accessibilityAddTraits(isSelected ? .isSelected : []) diff --git a/macos/Sources/Lithe/Views/Agent/AgentTranscriptView.swift b/macos/Sources/Lithe/Views/Agent/AgentTranscriptView.swift index e864c590f..2fb8d30d3 100644 --- a/macos/Sources/Lithe/Views/Agent/AgentTranscriptView.swift +++ b/macos/Sources/Lithe/Views/Agent/AgentTranscriptView.swift @@ -38,7 +38,7 @@ struct AgentTranscriptView: View { .frame(height: 26) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .litheRowHover() } } @@ -115,7 +115,7 @@ struct AgentHeroView: View { AgentBrandIcon(name: agentName, size: 60) .foregroundStyle(isHovering ? AgentPanelStyle.secondary : AgentPanelStyle.logo) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .accessibilityLabel("Switch Agent") .onHover { isHovering = $0 } @@ -271,7 +271,7 @@ private struct AgentToolCallRow: View { Button { expanded.toggle() } label: { header } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help(expanded ? "Hide tool details" : "Show tool details") if expanded && !message.toolDetails.isEmpty { AgentToolEvidenceView(details: message.toolDetails, onOpenFile: onOpenFile) @@ -337,7 +337,7 @@ private struct AgentToolEvidenceView: View { .lineLimit(2) .truncationMode(.middle) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .font(.system(size: 11, design: .monospaced)) .help(location.path) } @@ -452,7 +452,7 @@ private struct AgentCodeBlock: View { Image(systemName: didCopy ? "checkmark" : "doc.on.doc") .font(.system(size: 10.5)) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .foregroundStyle(didCopy ? LitheTheme.success : LitheTheme.tertiaryText) .help("Copy code") diff --git a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift index d017b8c68..eb1cebbcb 100644 --- a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift @@ -133,7 +133,7 @@ struct KeyboardShortcutSettingsView: View { .accessibilityLabel("Search actions or shortcuts") } .font(.system(size: 12)) - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.secondaryText) .padding(.horizontal, 16) .frame(height: 36) @@ -164,7 +164,7 @@ struct KeyboardShortcutSettingsView: View { .frame(height: 24) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(!query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) if isExpanded(section.group) { @@ -193,7 +193,7 @@ struct KeyboardShortcutSettingsView: View { } .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) shortcutControls(for: command) } @@ -247,7 +247,7 @@ struct KeyboardShortcutSettingsView: View { return HStack(spacing: 6) { if bindings.isEmpty { Button("Not Assigned") { beginEditing(commandID: command.id, bindingIndex: nil) } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .font(.system(size: 12)) .foregroundStyle(LitheTheme.tertiaryText) .lithePointer() @@ -277,7 +277,7 @@ struct KeyboardShortcutSettingsView: View { } } } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help("Edit Shortcut") .accessibilityLabel(binding.displayText) diff --git a/macos/Sources/Lithe/Views/App/PluginManagementView.swift b/macos/Sources/Lithe/Views/App/PluginManagementView.swift index 54b968366..2443fe1a5 100644 --- a/macos/Sources/Lithe/Views/App/PluginManagementView.swift +++ b/macos/Sources/Lithe/Views/App/PluginManagementView.swift @@ -101,7 +101,7 @@ struct PluginManagementView: View { .font(.system(size: 13, weight: .medium)) Spacer() Button(LocalizedStringKey("Install")) { model.installPluginPackage() } - .buttonStyle(.borderless).foregroundStyle(LitheTheme.accent) + .buttonStyle(.litheNoPress).foregroundStyle(LitheTheme.accent) } .padding(.horizontal, 14).frame(height: 38).background(LitheTheme.raised) ScrollView { @@ -160,7 +160,7 @@ struct PluginManagementView: View { .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .accessibilityValue(showsLanguageExtensions ? Text("Expanded") : Text("Collapsed")) .lithePointer() } diff --git a/macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift b/macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift index c2b74267e..5e3f984b4 100644 --- a/macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift @@ -129,7 +129,7 @@ struct ProjectRuntimeSettingsView: View { .clipShape(RoundedRectangle(cornerRadius: LitheTheme.Metrics.cornerRadius)) .contentShape(Rectangle()) } - .buttonStyle(LitheTreeRowButtonStyle()) + .buttonStyle(.litheNoPress) .foregroundStyle(isSelected ? LitheTheme.settingsSelectionText : LitheTheme.primaryText) .padding(.leading, subproject.parentID == nil ? 0 : 12) } diff --git a/macos/Sources/Lithe/Views/App/RunConfigurationSettingsView.swift b/macos/Sources/Lithe/Views/App/RunConfigurationSettingsView.swift index 0cc370d76..923e87eee 100644 --- a/macos/Sources/Lithe/Views/App/RunConfigurationSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/RunConfigurationSettingsView.swift @@ -68,7 +68,7 @@ private struct RunConfigurationSettingsContent: View { .padding(10) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(feature.configurationStatus != .ready || feature.isLoadingProject || isGenerating) } } diff --git a/macos/Sources/Lithe/Views/App/SettingsView.swift b/macos/Sources/Lithe/Views/App/SettingsView.swift index 0974431ad..f5db3d5ee 100644 --- a/macos/Sources/Lithe/Views/App/SettingsView.swift +++ b/macos/Sources/Lithe/Views/App/SettingsView.swift @@ -162,7 +162,7 @@ struct SettingsView: View { .clipShape(RoundedRectangle(cornerRadius: LitheTheme.Metrics.cornerRadius)) .contentShape(Rectangle()) } - .buttonStyle(LitheTreeRowButtonStyle()) + .buttonStyle(.litheNoPress) .foregroundStyle(isSelected ? LitheTheme.settingsSelectionText : LitheTheme.primaryText) } @@ -366,7 +366,7 @@ struct SettingsView: View { Button("Remove") { model.workbenchBackgroundFeature.clear() } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.accent) .lithePointer() } @@ -385,7 +385,7 @@ struct SettingsView: View { Button("Retry") { model.workbenchBackgroundFeature.retry() } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.accent) .lithePointer() } @@ -524,7 +524,7 @@ struct SettingsView: View { .font(.system(size: 16, weight: .regular)) .frame(width: 26, height: 26) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.secondaryText) .contentShape(Rectangle()) .lithePointer() @@ -552,7 +552,7 @@ struct SettingsView: View { } label: { Text("Restore Default") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.accent) .lithePointer() } @@ -1083,7 +1083,7 @@ struct SettingsView: View { border: viewState.isFormatPickerPresented ? LitheTheme.settingsControlAccent : LitheTheme.settingsControlBorder ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .popover(isPresented: $viewState.isFormatPickerPresented, arrowEdge: .bottom) { formatPickerPopover @@ -1192,7 +1192,7 @@ struct SettingsView: View { } .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .litheRowHover( isActive: isSelected, cornerRadius: 5, diff --git a/macos/Sources/Lithe/Views/App/StableRollbackControl.swift b/macos/Sources/Lithe/Views/App/StableRollbackControl.swift index 17135a49b..88a49b2ae 100644 --- a/macos/Sources/Lithe/Views/App/StableRollbackControl.swift +++ b/macos/Sources/Lithe/Views/App/StableRollbackControl.swift @@ -10,7 +10,7 @@ struct StableRollbackControl: View { if compact, updateChecker.isPreview { Button("Return to Stable") { showingDetails = true } .font(LitheTheme.smallFont) - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .sheet(isPresented: $showingDetails) { VStack(alignment: .leading, spacing: 16) { Text("Return to Stable").font(.headline) diff --git a/macos/Sources/Lithe/Views/App/UpdateControl.swift b/macos/Sources/Lithe/Views/App/UpdateControl.swift index 9963f36ae..8447e4fc3 100644 --- a/macos/Sources/Lithe/Views/App/UpdateControl.swift +++ b/macos/Sources/Lithe/Views/App/UpdateControl.swift @@ -76,7 +76,7 @@ struct UpdateControl: View { } label: { Label(compact ? "Update failed" : "Retry update", systemImage: "exclamationmark.triangle") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.warning) .help(message) case .idle, .upToDate: @@ -85,7 +85,7 @@ struct UpdateControl: View { } label: { Label("Check for Updates", systemImage: "arrow.clockwise") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.secondaryText) } } diff --git a/macos/Sources/Lithe/Views/App/WelcomeView.swift b/macos/Sources/Lithe/Views/App/WelcomeView.swift index 6a0473ebd..14930a880 100644 --- a/macos/Sources/Lithe/Views/App/WelcomeView.swift +++ b/macos/Sources/Lithe/Views/App/WelcomeView.swift @@ -68,7 +68,7 @@ struct WelcomeView: View { .frame(height: 34) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .litheRowHover() .padding(.horizontal, 14) @@ -184,7 +184,7 @@ struct WelcomeView: View { .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .disabled(!exists) diff --git a/macos/Sources/Lithe/Views/Components/LitheContextMenu.swift b/macos/Sources/Lithe/Views/Components/LitheContextMenu.swift index 6493d464d..a51611e85 100644 --- a/macos/Sources/Lithe/Views/Components/LitheContextMenu.swift +++ b/macos/Sources/Lithe/Views/Components/LitheContextMenu.swift @@ -278,7 +278,7 @@ private struct LitheContextMenuRow: View { } .padding(.horizontal, 5) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(!item.isEnabled) .opacity(item.isEnabled ? 1 : 0.45) .onHover { hovering in diff --git a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift index 1ecb6dbc9..ffddb4130 100644 --- a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift +++ b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift @@ -62,7 +62,7 @@ struct LitheSettingsSearchField: View { .font(.system(size: 11)) .foregroundStyle(LitheTheme.tertiaryText) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help("Clear search") } @@ -137,7 +137,7 @@ struct LitheSettingsSelect: View { .background(LitheSettingsSelectAnchorView(reference: popupAnchor)) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .accessibilityLabel(Text(LocalizedStringKey(accessibilityLabel))) .onChange(of: options) { _ in @@ -321,7 +321,7 @@ private struct LitheSettingsSelectPopupContent: View { ) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .onHover { hovering in if hovering { state.highlightedIndex = index } } @@ -490,7 +490,7 @@ struct LitheSettingsSegmentedControl: View { activeBackground: LitheTheme.settingsSelection ) } - .buttonStyle(LitheTreeRowButtonStyle()) + .buttonStyle(.litheNoPress) .lithePointer() .accessibilityValue(selection == option ? Text("Selected") : Text("Not selected")) } @@ -544,7 +544,7 @@ struct LitheSettingsCheckbox: View { } .contentShape(Rectangle()) } - .buttonStyle(LitheTreeRowButtonStyle()) + .buttonStyle(.litheNoPress) .lithePointer() .accessibilityRepresentation { Toggle(accessibilityLabel, isOn: $isOn) @@ -617,7 +617,7 @@ struct LitheSettingsStepper: View where Value: Strideable & Comparable, V .contentShape(Rectangle()) .litheRowHover(cornerRadius: 0) } - .buttonStyle(LitheTreeRowButtonStyle()) + .buttonStyle(.litheNoPress) .disabled(isDisabled) .lithePointer() } diff --git a/macos/Sources/Lithe/Views/Database/DatabaseSQLWorkspaceView.swift b/macos/Sources/Lithe/Views/Database/DatabaseSQLWorkspaceView.swift index c3932833e..1aff54c29 100644 --- a/macos/Sources/Lithe/Views/Database/DatabaseSQLWorkspaceView.swift +++ b/macos/Sources/Lithe/Views/Database/DatabaseSQLWorkspaceView.swift @@ -179,7 +179,7 @@ private struct DatabaseDashboardView: View { .frame(maxWidth: .infinity, alignment: .leading) .frame(height: 36) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(profiles.first(where: { $0.id == entry.profileID })?.kind.isSQLDatabase != true) .litheRowHover(cornerRadius: 0) } @@ -222,7 +222,7 @@ private struct DatabaseDashboardView: View { .stroke(LitheTheme.panelBorder, lineWidth: 1) } } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } .frame(maxWidth: .infinity) @@ -251,7 +251,7 @@ private struct DatabaseDashboardView: View { .frame(height: 48) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .litheRowHover(cornerRadius: 0) } @@ -278,7 +278,7 @@ private struct DatabaseDashboardView: View { .frame(height: 48) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) if profile.kind.isSQLDatabase { Button { onNewQuery(profile) } label: { @@ -288,7 +288,7 @@ private struct DatabaseDashboardView: View { .frame(width: 40, height: 40) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help("New Query") .accessibilityLabel("New Query") @@ -360,7 +360,7 @@ private struct DatabaseDashboardView: View { .frame(height: 48) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(!isEnabled) .opacity(isEnabled ? 1 : 0.42) .background(LitheTheme.raised) @@ -448,7 +448,7 @@ private struct DatabaseHistoryView: View { } .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) } .listStyle(.inset) } @@ -540,7 +540,7 @@ struct DatabaseSQLWorkspaceView: View { .padding(.leading, 10) .frame(height: 31) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) Button { model.databaseFeature.closeSQLTab(tab.id) } label: { Image(systemName: "xmark") diff --git a/macos/Sources/Lithe/Views/Database/DatabaseSidebarView.swift b/macos/Sources/Lithe/Views/Database/DatabaseSidebarView.swift index 713927a75..1c346ec5c 100644 --- a/macos/Sources/Lithe/Views/Database/DatabaseSidebarView.swift +++ b/macos/Sources/Lithe/Views/Database/DatabaseSidebarView.swift @@ -133,7 +133,7 @@ struct DatabaseSidebarView: View { .font(.system(size: 11)) .foregroundStyle(LitheTheme.tertiaryText) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help("Clear search") } } @@ -420,7 +420,7 @@ struct DatabaseSidebarView: View { .frame(height: 31) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) .lithePointer() @@ -486,7 +486,7 @@ struct DatabaseSidebarView: View { .foregroundStyle(LitheTheme.tertiaryText) .frame(width: 18, height: 30) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() Button { @@ -522,7 +522,7 @@ struct DatabaseSidebarView: View { .frame(height: 30) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) .lithePointer() @@ -572,7 +572,7 @@ struct DatabaseSidebarView: View { .frame(maxWidth: .infinity, minHeight: 30, alignment: .leading) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) .lithePointer() @@ -659,7 +659,7 @@ struct DatabaseSidebarView: View { .frame(maxWidth: .infinity, minHeight: 30, alignment: .leading) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) .lithePointer() @@ -751,7 +751,7 @@ struct DatabaseSidebarView: View { .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) .lithePointer() @@ -1464,7 +1464,7 @@ private struct DatabaseDBXImportSheet: View { .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(candidate.isDuplicate) .background(isSelected ? LitheTheme.accent.opacity(0.08) : LitheTheme.inputBackground.opacity(0.35)) .clipShape(RoundedRectangle(cornerRadius: 6)) diff --git a/macos/Sources/Lithe/Views/Database/DatabaseSpecializedWorkspaceViews.swift b/macos/Sources/Lithe/Views/Database/DatabaseSpecializedWorkspaceViews.swift index 0ff7fccd5..3674a9af3 100644 --- a/macos/Sources/Lithe/Views/Database/DatabaseSpecializedWorkspaceViews.swift +++ b/macos/Sources/Lithe/Views/Database/DatabaseSpecializedWorkspaceViews.swift @@ -162,7 +162,7 @@ struct RedisWorkspaceView: View { } .padding(.horizontal, 8).frame(height: 40).frame(maxWidth: .infinity, alignment: .leading) } - .buttonStyle(.plain).lithePointer() + .buttonStyle(.litheNoPress).lithePointer() .litheRowHover(isActive: feature.redisSelectedKey?.key == key.key, activeBackground: LitheTheme.subtleSelection) } } @@ -177,7 +177,7 @@ struct RedisWorkspaceView: View { .font(.system(size: 10.5, weight: .medium)) .frame(maxWidth: .infinity).padding(.vertical, 9) } - .buttonStyle(.plain).lithePointer() + .buttonStyle(.litheNoPress).lithePointer() .foregroundStyle(LitheTheme.accent) .disabled(feature.isLoading) } @@ -468,7 +468,7 @@ struct NacosWorkspaceView: View { } .padding(.horizontal, 9).frame(height: 42).frame(maxWidth: .infinity, alignment: .leading) } - .buttonStyle(.plain).lithePointer() + .buttonStyle(.litheNoPress).lithePointer() .litheRowHover(isActive: feature.nacosSelectedConfig?.dataId == config.dataId && feature.nacosSelectedConfig?.group == config.group, activeBackground: LitheTheme.subtleSelection) } }.padding(.vertical, 6) @@ -532,7 +532,7 @@ struct NacosWorkspaceView: View { } .padding(.horizontal, 9).frame(height: 42).frame(maxWidth: .infinity, alignment: .leading) } - .buttonStyle(.plain).lithePointer() + .buttonStyle(.litheNoPress).lithePointer() .litheRowHover(isActive: false, activeBackground: LitheTheme.subtleSelection) } }.padding(.vertical, 6) diff --git a/macos/Sources/Lithe/Views/Database/DatabaseTableView.swift b/macos/Sources/Lithe/Views/Database/DatabaseTableView.swift index de45a941b..003d5d765 100644 --- a/macos/Sources/Lithe/Views/Database/DatabaseTableView.swift +++ b/macos/Sources/Lithe/Views/Database/DatabaseTableView.swift @@ -179,7 +179,7 @@ struct DatabaseTableView: View { Button { model.databaseFeature.workspaceSection = .structure } label: { toolbarActionLabel("Table Properties", systemImage: "tablecells") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(model.databaseFeature.selectedProfile?.kind == .mongodb) toolbarDivider @@ -242,7 +242,7 @@ struct DatabaseTableView: View { Spacer() if !appliedFilters.isEmpty || !appliedSort.isEmpty { Button("Clear query") { clearQuery() } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .font(.system(size: 10, weight: .medium)) .foregroundStyle(LitheTheme.secondaryText) } @@ -257,7 +257,7 @@ struct DatabaseTableView: View { HStack(spacing: 7) { Group { Button { refreshTable() } label: { Label("Refresh table data", systemImage: "arrow.clockwise") } - .buttonStyle(.plain).font(.system(size: 10.5, weight: .medium)) + .buttonStyle(.litheNoPress).font(.system(size: 10.5, weight: .medium)) Menu { ForEach(model.databaseFeature.columns, id: \.self) { column in Button(column) { jumpTargetColumn = column } @@ -280,13 +280,13 @@ struct DatabaseTableView: View { .accessibilityLabel("Row details") .disabled(selectedRows.count != 1) Button { insertedRows.append([:]) } label: { Label("Add Row", systemImage: "plus") } - .buttonStyle(.plain).font(.system(size: 10.5, weight: .medium)) + .buttonStyle(.litheNoPress).font(.system(size: 10.5, weight: .medium)) .disabled(model.databaseFeature.selectedProfile?.readOnly == true) Button { apply() } label: { Label("Apply", systemImage: "checkmark") } - .buttonStyle(.plain).font(.system(size: 10.5, weight: .medium)) + .buttonStyle(.litheNoPress).font(.system(size: 10.5, weight: .medium)) .disabled(!hasChanges || model.databaseFeature.isLoading || model.databaseFeature.selectedProfile?.readOnly == true) Button { discard() } label: { Label("Discard", systemImage: "arrow.uturn.backward") } - .buttonStyle(.plain).font(.system(size: 10.5, weight: .medium)) + .buttonStyle(.litheNoPress).font(.system(size: 10.5, weight: .medium)) .disabled(!hasChanges) } if !selectedRows.isEmpty { @@ -300,13 +300,13 @@ struct DatabaseTableView: View { Button { showsBatchUpdateSheet = true } label: { Label("Batch Edit", systemImage: "square.and.pencil") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .font(.system(size: 10.5, weight: .medium)) .disabled(model.databaseFeature.selectedProfile?.readOnly == true) Button { showsBatchDeleteConfirmation = true } label: { Label("Delete Selected", systemImage: "trash") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .font(.system(size: 10.5, weight: .medium)) .foregroundStyle(LitheTheme.error) .disabled(model.databaseFeature.selectedProfile?.readOnly == true) @@ -337,7 +337,7 @@ struct DatabaseTableView: View { Button { filterConditions.append(.init(column: model.databaseFeature.columns.first ?? "")) } label: { Label("Add Condition", systemImage: "plus") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) } ForEach($filterConditions) { $condition in HStack(spacing: 8) { @@ -345,7 +345,7 @@ struct DatabaseTableView: View { Image(systemName: condition.isEnabled ? "checkmark.circle.fill" : "circle") .foregroundStyle(condition.isEnabled ? LitheTheme.accent : LitheTheme.tertiaryText) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help(condition.isEnabled ? "Disable condition" : "Enable condition") Picker("Column", selection: $condition.column) { ForEach(model.databaseFeature.columns, id: \.self) { Text($0).tag($0) } @@ -362,7 +362,7 @@ struct DatabaseTableView: View { Button(role: .destructive) { filterConditions.removeAll { $0.id == condition.id } } label: { Image(systemName: "trash") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) } } HStack { @@ -385,7 +385,7 @@ struct DatabaseTableView: View { Button { sortConditions.append(.init(column: model.databaseFeature.columns.first ?? "")) } label: { Label("Add Sort", systemImage: "plus") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) } if sortConditions.isEmpty { Text("No sorting") @@ -406,11 +406,11 @@ struct DatabaseTableView: View { } .frame(width: 120) Button { moveSort(from: index, by: -1) } label: { Image(systemName: "arrow.up") } - .buttonStyle(.plain).help("Move up").disabled(index == 0) + .buttonStyle(.litheNoPress).help("Move up").disabled(index == 0) Button { moveSort(from: index, by: 1) } label: { Image(systemName: "arrow.down") } - .buttonStyle(.plain).help("Move down").disabled(index == sortConditions.count - 1) + .buttonStyle(.litheNoPress).help("Move down").disabled(index == sortConditions.count - 1) Button(role: .destructive) { sortConditions.remove(at: index) } label: { Image(systemName: "trash") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) } } HStack { @@ -464,7 +464,7 @@ struct DatabaseTableView: View { } .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) } private func toolbarActionLabel( @@ -536,7 +536,7 @@ struct DatabaseTableView: View { .frame(width: selectionColumnWidth, height: 32) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help(allRowsSelected ? "Deselect all rows on this page" : "Select all rows on this page") ForEach(model.databaseFeature.columns, id: \.self) { column in HStack(spacing: 4) { @@ -596,7 +596,7 @@ struct DatabaseTableView: View { .frame(width: selectionColumnWidth, height: 29) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .background(LitheTheme.toolHeader) .disabled(deletedRows.contains(index)) .litheContextMenu { rowContextMenu(index: index) } @@ -981,7 +981,7 @@ struct DatabaseOpenTableTabsView: View { Button(table) { Task { await model.databaseFeature.openTable(table) } } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .font(.system(size: 11, weight: .medium)) Button { let wasSelected = model.databaseFeature.selectedTable == table @@ -992,7 +992,7 @@ struct DatabaseOpenTableTabsView: View { Image(systemName: "xmark") .font(.system(size: 8.5, weight: .semibold)) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help("Close table") } .padding(.horizontal, 9) diff --git a/macos/Sources/Lithe/Views/Debug/GenericDebugView.swift b/macos/Sources/Lithe/Views/Debug/GenericDebugView.swift index 71a7c5e83..a92d69e74 100644 --- a/macos/Sources/Lithe/Views/Debug/GenericDebugView.swift +++ b/macos/Sources/Lithe/Views/Debug/GenericDebugView.swift @@ -122,7 +122,7 @@ struct GenericDebugView: View { } .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } Spacer(minLength: 0) @@ -192,7 +192,7 @@ struct GenericDebugView: View { .foregroundStyle(LitheTheme.secondaryText) .frame(width: 16, height: 16) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help("Stop debug session") } @@ -487,7 +487,7 @@ struct GenericDebugView: View { .foregroundStyle(LitheTheme.secondaryText.opacity(0.82)) .lineLimit(1) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help("Reveal stopped location in editor") .accessibilityLabel("Reveal stopped location in editor") } @@ -581,7 +581,7 @@ struct GenericDebugView: View { feature.smartStepInto(target) isSmartStepPickerPresented = false } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .font(.system(size: 11, design: .monospaced)) .padding(.horizontal, 8) .padding(.vertical, 4) @@ -624,7 +624,7 @@ struct GenericDebugView: View { .frame(minHeight: 27) .frame(maxWidth: .infinity, alignment: .leading) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) } ForEach(feature.visibleStackFrameRows) { row in if let frame = row.frame { @@ -696,7 +696,7 @@ struct GenericDebugView: View { .frame(minHeight: 27) .frame(maxWidth: .infinity, alignment: .leading) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help("Show JDK, proxy, and framework frames") } } @@ -1124,13 +1124,13 @@ struct GenericDebugView: View { preservesOriginalColors: true ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(feature.state != .paused || feature.watches.isEmpty) .help("Refresh watches") Button { watchEditor = WatchEditorContext(watch: nil) } label: { Image(systemName: "plus") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help("Add watch") } .padding(.horizontal, 10) @@ -1436,7 +1436,7 @@ struct GenericDebugView: View { .frame(minHeight: 27) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(isLoading) .accessibilityLabel(isLoading ? "Loading debugger variables" : "Load more debugger variables") } @@ -1469,7 +1469,7 @@ struct GenericDebugView: View { .background(selected ? LitheTheme.selection : Color.clear) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) } } @@ -1699,7 +1699,7 @@ struct DebugBreakpointManagerView: View { } label: { Image(systemName: "plus") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help("Add method breakpoint") .accessibilityLabel("Add method breakpoint") } @@ -1731,7 +1731,7 @@ struct DebugBreakpointManagerView: View { ) } } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help(breakpoint.enabled ? "Disable breakpoint" : "Enable breakpoint") .accessibilityLabel(breakpoint.enabled ? "Disable breakpoint" : "Enable breakpoint") Button { @@ -1755,7 +1755,7 @@ struct DebugBreakpointManagerView: View { } .frame(maxWidth: .infinity, alignment: .leading) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .accessibilityLabel("Open \(breakpoint.title)") Menu { Button("Edit…") { editingBreakpoint = breakpoint } @@ -1804,7 +1804,7 @@ struct DebugBreakpointManagerView: View { .font(.system(size: 10)) .foregroundStyle(breakpoint.enabled ? LitheTheme.error : LitheTheme.secondaryText) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .accessibilityLabel( breakpoint.enabled ? "Disable \(breakpoint.label) exception breakpoint" @@ -1824,7 +1824,7 @@ struct DebugBreakpointManagerView: View { Button { editingExceptionBreakpoint = breakpoint } label: { Image(systemName: "ellipsis") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help("Edit exception breakpoint") .accessibilityLabel("Edit \(breakpoint.label) exception breakpoint") } @@ -1850,7 +1850,7 @@ struct DebugBreakpointManagerView: View { : LitheTheme.secondaryText ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .accessibilityLabel( breakpoint.enabled ? "Disable \(breakpoint.name) method breakpoint" @@ -1872,7 +1872,7 @@ struct DebugBreakpointManagerView: View { } .frame(maxWidth: .infinity, alignment: .leading) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .accessibilityLabel("Edit \(breakpoint.name) method breakpoint") Menu { Button("Edit…") { @@ -1909,7 +1909,7 @@ struct DebugBreakpointManagerView: View { preservesOriginalColors: true ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .accessibilityLabel( breakpoint.enabled ? "Disable \(breakpoint.label) field breakpoint" @@ -1927,7 +1927,7 @@ struct DebugBreakpointManagerView: View { } .frame(maxWidth: .infinity, alignment: .leading) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .accessibilityLabel("Edit \(breakpoint.label) field breakpoint") Menu { Button("Edit…") { editingDataBreakpoint = breakpoint } diff --git a/macos/Sources/Lithe/Views/Diff/DiffCollapsedBandView.swift b/macos/Sources/Lithe/Views/Diff/DiffCollapsedBandView.swift index 7af7c80c0..1e53e63b6 100644 --- a/macos/Sources/Lithe/Views/Diff/DiffCollapsedBandView.swift +++ b/macos/Sources/Lithe/Views/Diff/DiffCollapsedBandView.swift @@ -24,7 +24,7 @@ struct DiffCollapsedBandView: View { .background(LitheTheme.window) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help("Expand these lines") } diff --git a/macos/Sources/Lithe/Views/Diff/DiffReviewView.swift b/macos/Sources/Lithe/Views/Diff/DiffReviewView.swift index 17ab7058b..167d52bdf 100644 --- a/macos/Sources/Lithe/Views/Diff/DiffReviewView.swift +++ b/macos/Sources/Lithe/Views/Diff/DiffReviewView.swift @@ -159,7 +159,7 @@ struct DiffReviewView: View { systemImage: highlightsWords ? "checkmark.square.fill" : "square" ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help(highlightsWords ? "Disable word-level highlights" : "Enable word-level highlights") @@ -172,7 +172,7 @@ struct DiffReviewView: View { systemImage: collapsesUnchangedRegions ? "checkmark.square.fill" : "square" ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help( collapsesUnchangedRegions diff --git a/macos/Sources/Lithe/Views/Editor/EditorAreaView.swift b/macos/Sources/Lithe/Views/Editor/EditorAreaView.swift index 868eb453a..45493c10a 100644 --- a/macos/Sources/Lithe/Views/Editor/EditorAreaView.swift +++ b/macos/Sources/Lithe/Views/Editor/EditorAreaView.swift @@ -471,7 +471,7 @@ struct EditorAreaView: View { .contentShape(Rectangle()) .litheRowHover(cornerRadius: 10) } - .buttonStyle(LitheTreeRowButtonStyle()) + .buttonStyle(.litheNoPress) .lithePointer() .foregroundStyle(LitheTheme.secondaryText) .opacity(isActive || hoveredTabItem == tabItem ? 1 : 0) @@ -563,7 +563,7 @@ struct EditorAreaView: View { .contentShape(Rectangle()) .litheRowHover(cornerRadius: 10) } - .buttonStyle(LitheTreeRowButtonStyle()) + .buttonStyle(.litheNoPress) .lithePointer() .foregroundStyle(LitheTheme.secondaryText) .opacity(isActive || hoveredTabItem == tabItem ? 1 : 0) @@ -676,7 +676,7 @@ struct EditorAreaView: View { .contentShape(Rectangle()) .litheRowHover(cornerRadius: 10) } - .buttonStyle(LitheTreeRowButtonStyle()) + .buttonStyle(.litheNoPress) .lithePointer() .foregroundStyle(LitheTheme.secondaryText) .opacity(isActive || hoveredTabItem == .document(document.id) ? 1 : 0) @@ -1073,7 +1073,7 @@ struct EditorAreaView: View { .opacity(isSelected || isHovered ? 1 : 0.72) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help(mode.title) .accessibilityLabel(mode.title) @@ -1133,7 +1133,7 @@ struct EditorAreaView: View { HStack { Spacer() Text(splitDocument.displayName).font(.system(size: 11)) - Button("Close split") { self.splitDocumentID = nil }.buttonStyle(.borderless) + Button("Close split") { self.splitDocumentID = nil }.buttonStyle(.litheNoPress) } .padding(.horizontal, 10).frame(height: 30) MonacoWorkbenchEditor(document: model.activeDocument ?? splitDocument, secondaryDocument: splitDocument) diff --git a/macos/Sources/Lithe/Views/Editor/HTMLPreviewView.swift b/macos/Sources/Lithe/Views/Editor/HTMLPreviewView.swift index 61ca2aa6d..504c0fcda 100644 --- a/macos/Sources/Lithe/Views/Editor/HTMLPreviewView.swift +++ b/macos/Sources/Lithe/Views/Editor/HTMLPreviewView.swift @@ -27,7 +27,7 @@ struct HTMLPreviewView: View { .font(.system(size: 12, weight: .medium)) .frame(width: 24, height: 24) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.secondaryText) .background(LitheTheme.toolHeader.opacity(0.92)) .clipShape(RoundedRectangle(cornerRadius: 4)) diff --git a/macos/Sources/Lithe/Views/Editor/MarkdownPreviewView.swift b/macos/Sources/Lithe/Views/Editor/MarkdownPreviewView.swift index cf81f5801..32f3307ce 100644 --- a/macos/Sources/Lithe/Views/Editor/MarkdownPreviewView.swift +++ b/macos/Sources/Lithe/Views/Editor/MarkdownPreviewView.swift @@ -55,7 +55,7 @@ struct MarkdownPreviewView: View { Button("Retry") { scheduleRender(immediate: true) } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .font(.system(size: 12)) } .padding(16) diff --git a/macos/Sources/Lithe/Views/Git/BranchComparisonView.swift b/macos/Sources/Lithe/Views/Git/BranchComparisonView.swift index 3fd7c2fc4..e7f49d053 100644 --- a/macos/Sources/Lithe/Views/Git/BranchComparisonView.swift +++ b/macos/Sources/Lithe/Views/Git/BranchComparisonView.swift @@ -169,7 +169,7 @@ struct BranchComparisonView: View { ) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } } diff --git a/macos/Sources/Lithe/Views/Git/BranchSwitcherPopover.swift b/macos/Sources/Lithe/Views/Git/BranchSwitcherPopover.swift index b8155927c..3bcf4b9ab 100644 --- a/macos/Sources/Lithe/Views/Git/BranchSwitcherPopover.swift +++ b/macos/Sources/Lithe/Views/Git/BranchSwitcherPopover.swift @@ -65,7 +65,7 @@ struct BranchSwitcherPopover: View { .font(.system(size: 12)) .foregroundStyle(LitheTheme.secondaryText) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help("Clear search") } @@ -243,7 +243,7 @@ struct BranchSwitcherPopover: View { ) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } @@ -329,7 +329,7 @@ struct BranchSwitcherPopover: View { .contentShape(Rectangle()) .litheRowHover(cornerRadius: 5, hoverBackground: LitheTheme.subtleSelection) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } @@ -363,7 +363,7 @@ struct BranchSwitcherPopover: View { .contentShape(Rectangle()) .litheRowHover(cornerRadius: 5, hoverBackground: LitheTheme.subtleSelection) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } diff --git a/macos/Sources/Lithe/Views/Git/ChangesSidebarView.swift b/macos/Sources/Lithe/Views/Git/ChangesSidebarView.swift index 70233a03d..3bf9876bb 100644 --- a/macos/Sources/Lithe/Views/Git/ChangesSidebarView.swift +++ b/macos/Sources/Lithe/Views/Git/ChangesSidebarView.swift @@ -161,7 +161,7 @@ struct ChangesSidebarView: View { hoverBackground: LitheTheme.hoverBackground ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) } Spacer() GitPatchToolbar(feature: feature) @@ -344,7 +344,7 @@ struct ChangesSidebarView: View { .clipShape(RoundedRectangle(cornerRadius: 4)) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .litheContextMenu { [ @@ -402,7 +402,7 @@ struct ChangesSidebarView: View { .clipShape(RoundedRectangle(cornerRadius: 4)) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .litheContextMenu { [ @@ -461,7 +461,7 @@ struct ChangesSidebarView: View { } label: { Label("Clear conflict filter", systemImage: "line.3.horizontal.decrease.circle") } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .font(.system(size: 10.5)) .foregroundStyle(LitheTheme.warning) .lithePointer() @@ -510,7 +510,7 @@ struct ChangesSidebarView: View { .foregroundStyle(LitheTheme.warning) Text("No files match the conflict filter") Button("Show all changes") { feature.clearGitConflictFilter() } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .lithePointer() } .font(LitheTheme.uiFont) @@ -564,7 +564,7 @@ struct ChangesSidebarView: View { .frame(width: 10, height: 24) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help(LocalizedStringKey(expanded.wrappedValue ? "Collapse section" : "Expand section")) Button { @@ -576,7 +576,7 @@ struct ChangesSidebarView: View { .frame(width: 18, height: 24) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help(LocalizedStringKey(allChangesStaged(changes) ? "Unstage all files" : "Stage all files")) Button { @@ -594,7 +594,7 @@ struct ChangesSidebarView: View { .frame(maxWidth: .infinity, minHeight: 24) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) } .padding(.horizontal, 7) .frame(maxWidth: .infinity) @@ -642,7 +642,7 @@ struct ChangesSidebarView: View { .frame(width: 28, height: changeRowHeight) .contentShape(Rectangle()) } - .buttonStyle(LitheTreeRowButtonStyle()) + .buttonStyle(.litheNoPress) .help(LocalizedStringKey(isEffectivelyStaged(change) ? "Unstage file" : "Stage file")) Button { @@ -675,7 +675,7 @@ struct ChangesSidebarView: View { .frame(height: changeRowHeight) .contentShape(Rectangle()) } - .buttonStyle(LitheTreeRowButtonStyle()) + .buttonStyle(.litheNoPress) } .padding(.leading, 30) .padding(.trailing, 6) diff --git a/macos/Sources/Lithe/Views/Git/GitCommitDiffReviewView.swift b/macos/Sources/Lithe/Views/Git/GitCommitDiffReviewView.swift index 437491309..19ff74cba 100644 --- a/macos/Sources/Lithe/Views/Git/GitCommitDiffReviewView.swift +++ b/macos/Sources/Lithe/Views/Git/GitCommitDiffReviewView.swift @@ -152,7 +152,7 @@ struct GitCommitDiffReviewView: View { .clipShape(RoundedRectangle(cornerRadius: 5)) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() Text("Read-only") diff --git a/macos/Sources/Lithe/Views/Git/GitConsoleEntryView.swift b/macos/Sources/Lithe/Views/Git/GitConsoleEntryView.swift index d4c7d7ea0..5fd9989ab 100644 --- a/macos/Sources/Lithe/Views/Git/GitConsoleEntryView.swift +++ b/macos/Sources/Lithe/Views/Git/GitConsoleEntryView.swift @@ -39,7 +39,7 @@ struct GitConsoleEntryView: View { if fragment.matches > 0 { Text(" · \(fragment.matches) matches") } } } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.secondaryText) .accessibilityValue(isExpanded(fragment) ? Text("Expanded") : Text("Collapsed")) } diff --git a/macos/Sources/Lithe/Views/Git/GitConsoleView.swift b/macos/Sources/Lithe/Views/Git/GitConsoleView.swift index 22d9d9bfa..17b1a4aa1 100644 --- a/macos/Sources/Lithe/Views/Git/GitConsoleView.swift +++ b/macos/Sources/Lithe/Views/Git/GitConsoleView.swift @@ -77,7 +77,7 @@ struct GitConsoleView: View { .disabled(records.isEmpty).help("Copy complete console") Spacer(minLength: 0) } - .buttonStyle(.plain).litheIconButton() + .buttonStyle(.litheNoPress).litheIconButton() .foregroundStyle(LitheTheme.secondaryText) .padding(.top, 6).frame(width: 28) } diff --git a/macos/Sources/Lithe/Views/Git/GitGraphView.swift b/macos/Sources/Lithe/Views/Git/GitGraphView.swift index 1794ae8c8..f2f2b3dd4 100644 --- a/macos/Sources/Lithe/Views/Git/GitGraphView.swift +++ b/macos/Sources/Lithe/Views/Git/GitGraphView.swift @@ -98,7 +98,7 @@ struct GitGraphView: View { } label: { Color.clear.frame(width: rect.width, height: rect.height).contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help(title) .accessibilityLabel(title) .accessibilityIdentifier("git-graph-arrow-\(row.commit.hash)-\(element.id)") @@ -744,7 +744,7 @@ private struct GitGraphRowView: View, Equatable { .background(backgroundColor) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .accessibilityAddTraits(isSelected ? .isSelected : []) .onHover { isHovered = $0 } .litheContextMenu { actions.contextMenuItems(for: row.commit) } diff --git a/macos/Sources/Lithe/Views/Git/GitHistoryRewriteView.swift b/macos/Sources/Lithe/Views/Git/GitHistoryRewriteView.swift index a4b460ff1..d9cdc5c9b 100644 --- a/macos/Sources/Lithe/Views/Git/GitHistoryRewriteView.swift +++ b/macos/Sources/Lithe/Views/Git/GitHistoryRewriteView.swift @@ -107,7 +107,7 @@ struct GitHistoryRewriteOutcomeView: View { } Spacer(minLength: 0) Button { editor.outcome = nil } label: { Image(systemName: "xmark") } - .buttonStyle(.plain).help("Dismiss history operation result").lithePointer() + .buttonStyle(.litheNoPress).help("Dismiss history operation result").lithePointer() } .padding(10) .background(LitheTheme.toolHeader) diff --git a/macos/Sources/Lithe/Views/Git/GitInteractiveRebaseStatusView.swift b/macos/Sources/Lithe/Views/Git/GitInteractiveRebaseStatusView.swift index 755858bfb..a8ca5e75f 100644 --- a/macos/Sources/Lithe/Views/Git/GitInteractiveRebaseStatusView.swift +++ b/macos/Sources/Lithe/Views/Git/GitInteractiveRebaseStatusView.swift @@ -39,7 +39,7 @@ private struct GitInteractiveRebaseSessionBanner: View { Spacer(minLength: 0) if !session.isActive { Button { editor.dismissSession() } label: { Image(systemName: "xmark") } - .buttonStyle(.plain).help("Dismiss rebase result").accessibilityLabel("Dismiss rebase result").lithePointer() + .buttonStyle(.litheNoPress).help("Dismiss rebase result").accessibilityLabel("Dismiss rebase result").lithePointer() } } Text("\(session.branch) · \(session.completedSteps) of \(session.steps.count) steps processed") diff --git a/macos/Sources/Lithe/Views/Git/GitLogFilterPopover.swift b/macos/Sources/Lithe/Views/Git/GitLogFilterPopover.swift index 043d87a34..a1fa67687 100644 --- a/macos/Sources/Lithe/Views/Git/GitLogFilterPopover.swift +++ b/macos/Sources/Lithe/Views/Git/GitLogFilterPopover.swift @@ -647,7 +647,7 @@ struct GitLogFilterRowView: View { .clipShape(RoundedRectangle(cornerRadius: 5)) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } @@ -867,7 +867,7 @@ struct GitLogBranchFilterPopover: View { .clipShape(RoundedRectangle(cornerRadius: 5)) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } diff --git a/macos/Sources/Lithe/Views/Git/GitLogView.swift b/macos/Sources/Lithe/Views/Git/GitLogView.swift index 910aed0cc..df9ddc1ba 100644 --- a/macos/Sources/Lithe/Views/Git/GitLogView.swift +++ b/macos/Sources/Lithe/Views/Git/GitLogView.swift @@ -500,7 +500,7 @@ struct GitLogView: View { .frame(height: 27) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() if showsCloseButton { @@ -513,7 +513,7 @@ struct GitLogView: View { .frame(width: 20, height: 27) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help("Close Git console") } @@ -795,7 +795,7 @@ struct GitLogView: View { .contentShape(Rectangle()) .litheRowHover(cornerRadius: 4) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() if !isCollapsed { @@ -868,7 +868,7 @@ struct GitLogView: View { .contentShape(Rectangle()) .litheRowHover(cornerRadius: 4) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() if expanded.wrappedValue { @@ -1037,7 +1037,7 @@ struct GitLogView: View { activeBackground: LitheTheme.subtleSelection ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .litheContextMenu { var items: [LitheContextMenuItem] = [] @@ -1253,7 +1253,7 @@ struct GitLogView: View { .frame(height: 32) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } } @@ -1611,7 +1611,7 @@ struct GitLogView: View { selection: feature.selectedGitReference?.shortName ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .overlay { GitLogInstantPopover(isPresented: $showsGitLogBranchFilterPopover) { @@ -1647,7 +1647,7 @@ struct GitLogView: View { } label: { gitLogFilterLabel(title: "User", selection: selectedGitLogAuthor?.displayName, localizeSelection: selectedGitLogAuthor == .currentUser) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .overlay { GitLogInstantPopover(isPresented: $showsGitLogAuthorFilterPopover) { @@ -1714,7 +1714,7 @@ struct GitLogView: View { selection: gitLogPathFilter.isEmpty ? nil : gitLogPathFilter ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .overlay { GitLogInstantPopover(isPresented: $showsGitLogPathPopover) { @@ -1800,7 +1800,7 @@ struct GitLogView: View { .frame(width: 14, height: 22) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help(LocalizedStringKey(help)) } @@ -2497,7 +2497,7 @@ private struct GitConflictPathRow: View { .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help("Show Diff") @@ -2508,7 +2508,7 @@ private struct GitConflictPathRow: View { Image(systemName: "arrow.uturn.backward") .font(.system(size: 10, weight: .semibold)) } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.warning) .lithePointer() .help("Discard this file and retry") @@ -2696,7 +2696,7 @@ struct GitPullStrategyDialog: View { } .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } } @@ -3018,7 +3018,7 @@ private struct GitReferenceRowView: View, Equatable { .contentShape(Rectangle()) .litheRowHover(cornerRadius: 4) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } @@ -3059,7 +3059,7 @@ private struct GitReferenceRowView: View, Equatable { activeBackground: LitheTheme.subtleSelection ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .litheContextMenu { referenceMenuItems(for: reference) diff --git a/macos/Sources/Lithe/Views/Git/GitPatchView.swift b/macos/Sources/Lithe/Views/Git/GitPatchView.swift index d6dd631dc..c34213f20 100644 --- a/macos/Sources/Lithe/Views/Git/GitPatchView.swift +++ b/macos/Sources/Lithe/Views/Git/GitPatchView.swift @@ -24,14 +24,14 @@ struct GitPatchToolbar: View { guard let root = feature.gitRepositoryRoot else { return } feature.patchExchange.beginExport(at: root) } label: { Image(systemName: "doc.badge.arrow.up") } - .buttonStyle(.plain).help("Create Patch…").lithePointer() + .buttonStyle(.litheNoPress).help("Create Patch…").lithePointer() .accessibilityLabel("Create Patch") .disabled(feature.gitRepositoryRoot == nil) Button { guard let root = feature.gitRepositoryRoot else { return } feature.patchExchange.beginImport(at: root, surface: .changes) } label: { Image(systemName: "doc.badge.arrow.down") } - .buttonStyle(.plain).help("Apply Patch…").lithePointer() + .buttonStyle(.litheNoPress).help("Apply Patch…").lithePointer() .accessibilityLabel("Apply Patch") .disabled(feature.gitRepositoryRoot == nil) } diff --git a/macos/Sources/Lithe/Views/Git/GitWorktreesView.swift b/macos/Sources/Lithe/Views/Git/GitWorktreesView.swift index 8881f0f59..543be53a8 100644 --- a/macos/Sources/Lithe/Views/Git/GitWorktreesView.swift +++ b/macos/Sources/Lithe/Views/Git/GitWorktreesView.swift @@ -371,7 +371,7 @@ struct GitWorktreesView: View { Image(systemName: "arrow.clockwise") .frame(width: 24, height: 24) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .disabled(feature.isPerformingWorktreeOperation) .help("Refresh worktrees") @@ -461,7 +461,7 @@ struct GitWorktreesView: View { Image(systemName: "doc.on.doc") .font(.system(size: 12)) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help("Copy worktree path") Spacer() @@ -496,7 +496,7 @@ struct GitWorktreesView: View { .frame(height: 2) } } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } @@ -547,7 +547,7 @@ struct GitWorktreesView: View { } label: { Image(systemName: "doc.on.doc") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } } @@ -867,7 +867,7 @@ struct GitWorktreesView: View { Label("Copy worktree path", systemImage: "arrow.right") .font(Visual.bodyMedium) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.link) .lithePointer() } diff --git a/macos/Sources/Lithe/Views/GitHub/GitHubPullRequestsView.swift b/macos/Sources/Lithe/Views/GitHub/GitHubPullRequestsView.swift index 65f2652b5..e6a710510 100644 --- a/macos/Sources/Lithe/Views/GitHub/GitHubPullRequestsView.swift +++ b/macos/Sources/Lithe/Views/GitHub/GitHubPullRequestsView.swift @@ -293,7 +293,7 @@ struct GitHubPullRequestsSidebarView: View { Button { searchQuery = "" } label: { Image(systemName: "xmark.circle.fill") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.tertiaryText) } } @@ -555,7 +555,7 @@ struct GitHubPullRequestDetailView: View { .background(selectedSection == section ? LitheTheme.subtleSelection : .clear) .clipShape(RoundedRectangle(cornerRadius: 5)) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } Spacer() @@ -859,7 +859,7 @@ private struct GitHubPullRequestRow: View { .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .litheRowHover( isActive: isSelected, @@ -1028,7 +1028,7 @@ private struct GitHubFileRow: View { .frame(height: 38) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() if isExpanded, let patch = file.patch { @@ -1081,7 +1081,7 @@ private struct GitHubCommentRow: View { Image(systemName: "arrow.up.right") .font(.system(size: 8, weight: .bold)) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.tertiaryText) } } @@ -1905,7 +1905,7 @@ private struct GitHubBranchPicker: View { ) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .accessibilityLabel(label) .accessibilityValue(selection.isEmpty ? Text("Select branch") : Text(selection)) .popover(isPresented: $isPresented, arrowEdge: .bottom) { @@ -1993,7 +1993,7 @@ private struct GitHubBranchPicker: View { .frame(maxWidth: .infinity, minHeight: 30, alignment: .leading) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) } private var filteredBranches: [GitHubBranch] { diff --git a/macos/Sources/Lithe/Views/History/LocalHistoryView.swift b/macos/Sources/Lithe/Views/History/LocalHistoryView.swift index b7195b40e..e069e91c1 100644 --- a/macos/Sources/Lithe/Views/History/LocalHistoryView.swift +++ b/macos/Sources/Lithe/Views/History/LocalHistoryView.swift @@ -131,7 +131,7 @@ struct LocalHistoryView: View { ) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } } diff --git a/macos/Sources/Lithe/Views/History/ProjectLocalHistoryView.swift b/macos/Sources/Lithe/Views/History/ProjectLocalHistoryView.swift index 372d54942..f3e6409f7 100644 --- a/macos/Sources/Lithe/Views/History/ProjectLocalHistoryView.swift +++ b/macos/Sources/Lithe/Views/History/ProjectLocalHistoryView.swift @@ -134,7 +134,7 @@ struct ProjectLocalHistoryView: View { ) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } } diff --git a/macos/Sources/Lithe/Views/Language/JavaProblemsView.swift b/macos/Sources/Lithe/Views/Language/JavaProblemsView.swift index af1d92992..d1564b18b 100644 --- a/macos/Sources/Lithe/Views/Language/JavaProblemsView.swift +++ b/macos/Sources/Lithe/Views/Language/JavaProblemsView.swift @@ -146,7 +146,7 @@ struct ProblemsView: View { .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help(diagnostic.detailText) } diff --git a/macos/Sources/Lithe/Views/Language/JavaReferencesView.swift b/macos/Sources/Lithe/Views/Language/JavaReferencesView.swift index 56a2a5d1f..17c107b1d 100644 --- a/macos/Sources/Lithe/Views/Language/JavaReferencesView.swift +++ b/macos/Sources/Lithe/Views/Language/JavaReferencesView.swift @@ -59,7 +59,7 @@ struct LanguageReferencesView: View { .frame(height: 30) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } } @@ -124,7 +124,7 @@ struct LanguageImplementationChooserView: View { .frame(height: 30) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } } diff --git a/macos/Sources/Lithe/Views/Language/LanguageServerSetupView.swift b/macos/Sources/Lithe/Views/Language/LanguageServerSetupView.swift index 32d32a9c1..87f7758e5 100644 --- a/macos/Sources/Lithe/Views/Language/LanguageServerSetupView.swift +++ b/macos/Sources/Lithe/Views/Language/LanguageServerSetupView.swift @@ -192,7 +192,7 @@ struct LanguageServerSetupView: View { Button(copy.useAutomatic) { clearOverride() } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .font(.system(size: 10.5, weight: .medium)) .foregroundStyle(LitheTheme.accent) .disabled(tools.customExecutablePath(for: selectedProviderID) == nil) diff --git a/macos/Sources/Lithe/Views/Language/LanguageTestsView.swift b/macos/Sources/Lithe/Views/Language/LanguageTestsView.swift index ab54ff04b..b3dc4ce9e 100644 --- a/macos/Sources/Lithe/Views/Language/LanguageTestsView.swift +++ b/macos/Sources/Lithe/Views/Language/LanguageTestsView.swift @@ -228,7 +228,7 @@ struct LanguageTestsView: View { .frame(height: 25) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .litheRowHover(isActive: false, cornerRadius: 5, activeBackground: LitheTheme.subtleSelection) if !isCollapsed { @@ -263,7 +263,7 @@ struct LanguageTestsView: View { activeBackground: LitheTheme.selection ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } @@ -449,7 +449,7 @@ struct LanguageTestsView: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(.vertical, 3) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(failure.fileURL == nil) } } diff --git a/macos/Sources/Lithe/Views/Run/JavaRunConfigurationEditorView.swift b/macos/Sources/Lithe/Views/Run/JavaRunConfigurationEditorView.swift index a35931cb1..e1881c768 100644 --- a/macos/Sources/Lithe/Views/Run/JavaRunConfigurationEditorView.swift +++ b/macos/Sources/Lithe/Views/Run/JavaRunConfigurationEditorView.swift @@ -53,7 +53,7 @@ struct RunConfigurationEditorView: View { options = RunOptions() environmentText = "" } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .lithePointer() .foregroundStyle(LitheTheme.secondaryText) Spacer() @@ -266,7 +266,7 @@ struct RunConfigurationEditorView: View { onClose() model.showSettings(category: .project) } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .lithePointer() } } diff --git a/macos/Sources/Lithe/Views/Run/MavenBuildOutputView.swift b/macos/Sources/Lithe/Views/Run/MavenBuildOutputView.swift index b1ea88aa6..bfa8de429 100644 --- a/macos/Sources/Lithe/Views/Run/MavenBuildOutputView.swift +++ b/macos/Sources/Lithe/Views/Run/MavenBuildOutputView.swift @@ -110,7 +110,7 @@ struct MavenBuildOutputView: View { .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } } diff --git a/macos/Sources/Lithe/Views/Run/MavenView.swift b/macos/Sources/Lithe/Views/Run/MavenView.swift index a46f09c63..18d8c4f3f 100644 --- a/macos/Sources/Lithe/Views/Run/MavenView.swift +++ b/macos/Sources/Lithe/Views/Run/MavenView.swift @@ -185,7 +185,7 @@ struct MavenView: View { Button(feature.isReloading ? String(localized: "Reloading Maven...") : String(localized: "Reload")) { Task { await model.reloadMavenProject(rescan: feature.isProjectReloadRequired) } } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .disabled(feature.isReloading) } .padding(.horizontal, 10) @@ -363,7 +363,7 @@ struct MavenView: View { Button(dependencyLocalization.text("Cancel")) { feature.cancelDependencies(for: modulePath) } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) } .font(.system(size: 11.5)) .foregroundStyle(LitheTheme.secondaryText) @@ -380,7 +380,7 @@ struct MavenView: View { Button(dependencyLocalization.text("Retry")) { feature.loadDependencies(for: modulePath) } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) } .padding(.horizontal, 4) .padding(.vertical, 3) @@ -394,7 +394,7 @@ struct MavenView: View { Button(dependencyLocalization.text("Retry")) { feature.loadDependencies(for: modulePath) } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) } .font(.system(size: 11.5)) .foregroundStyle(LitheTheme.warning) @@ -479,7 +479,7 @@ struct MavenView: View { .frame(minHeight: 28) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .padding(.leading, 16) .help(dependencyLocalization.text("Open module pom.xml")) @@ -550,7 +550,7 @@ struct MavenView: View { Image(systemName: "plus") .frame(width: 18, height: 20) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help("Add profile") .popover(isPresented: $isAddProfilePresented, arrowEdge: .trailing) { VStack(alignment: .leading, spacing: 10) { @@ -575,7 +575,7 @@ struct MavenView: View { Image(systemName: "arrow.uturn.backward") .frame(width: 18, height: 20) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help("Restore default profiles") Spacer(minLength: 0) } @@ -615,7 +615,7 @@ struct MavenView: View { .clipShape(RoundedRectangle(cornerRadius: 4)) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .simultaneousGesture(TapGesture(count: 2).onEnded { guard !model.isMavenOperationBusy else { return } @@ -655,7 +655,7 @@ struct MavenView: View { .frame(width: 14, height: 24) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() Button(action: onLabelAction) { @@ -685,7 +685,7 @@ struct MavenView: View { .clipShape(RoundedRectangle(cornerRadius: 4)) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } .litheContextMenu(items: { diff --git a/macos/Sources/Lithe/Views/Run/RunView.swift b/macos/Sources/Lithe/Views/Run/RunView.swift index 0a4122c47..ceada26bf 100644 --- a/macos/Sources/Lithe/Views/Run/RunView.swift +++ b/macos/Sources/Lithe/Views/Run/RunView.swift @@ -279,7 +279,7 @@ struct RunView: View { Button(action: runCheckedConfigurations) { Label("Run selected", systemImage: "play.fill") } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.success) .help("Run checked configurations without restarting running services") .disabled(feature.configurationStatus != .ready || feature.isLoadingProject) @@ -541,7 +541,7 @@ struct RunView: View { } .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } .font(.system(size: 11.5)) @@ -611,7 +611,7 @@ struct RunView: View { } .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help(isCollapsed ? "Expand" : "Collapse") .accessibilityValue(isCollapsed ? "Collapsed" : "Expanded") @@ -630,7 +630,7 @@ struct RunView: View { .frame(width: 18, height: 24) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .disabled(entries.isEmpty || feature.isLoadingProject) .help("Select configurations for batch actions") @@ -796,7 +796,7 @@ struct RunView: View { if contentTab == tab { Rectangle().fill(LitheTheme.accent).frame(height: 2) } } } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .accessibilityAddTraits(contentTab == tab ? .isSelected : []) } @@ -1058,7 +1058,7 @@ struct RunView: View { Text("localhost:" + portText) .underline() } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.accent) .help("Open \(serviceURL.absoluteString) in browser") .accessibilityLabel("Open service on port " + portText) @@ -1073,7 +1073,7 @@ struct RunView: View { Button(action: onPin) { Image(systemName: isPinned ? "pin.fill" : "pin") } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .frame(width: 20, height: 24) .contentShape(Rectangle()) .lithePointer() diff --git a/macos/Sources/Lithe/Views/Run/SpringEndpointsView.swift b/macos/Sources/Lithe/Views/Run/SpringEndpointsView.swift index 2cf781fcd..1e79463c1 100644 --- a/macos/Sources/Lithe/Views/Run/SpringEndpointsView.swift +++ b/macos/Sources/Lithe/Views/Run/SpringEndpointsView.swift @@ -55,7 +55,7 @@ struct SpringEndpointsView: View { .frame(maxWidth: .infinity, minHeight: 30) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } } diff --git a/macos/Sources/Lithe/Views/Search/ProjectReplaceView.swift b/macos/Sources/Lithe/Views/Search/ProjectReplaceView.swift index b5e0e44cf..957e99bd0 100644 --- a/macos/Sources/Lithe/Views/Search/ProjectReplaceView.swift +++ b/macos/Sources/Lithe/Views/Search/ProjectReplaceView.swift @@ -374,7 +374,7 @@ private struct ProjectReplaceOptionButton: View { .background(isOn || isHovering ? LitheTheme.subtleSelection : .clear) .clipShape(RoundedRectangle(cornerRadius: 3)) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(!isEnabled) .opacity(isEnabled ? 1 : 0.45) .onHover { isHovering = $0 } @@ -417,7 +417,7 @@ private struct ProjectReplaceCheckboxStyle: ToggleStyle { configuration.label } } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } } diff --git a/macos/Sources/Lithe/Views/Search/ProjectReplacementSourcePreview.swift b/macos/Sources/Lithe/Views/Search/ProjectReplacementSourcePreview.swift index 5c1e82fb8..5f7ec5d8a 100644 --- a/macos/Sources/Lithe/Views/Search/ProjectReplacementSourcePreview.swift +++ b/macos/Sources/Lithe/Views/Search/ProjectReplacementSourcePreview.swift @@ -80,7 +80,7 @@ private struct ProjectReplacementDocumentEditor: View { } } } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(saveTask != nil || !document.isDirty || document.isReadOnly) } .font(.system(size: 12)) diff --git a/macos/Sources/Lithe/Views/Search/SearchEverywhereView.swift b/macos/Sources/Lithe/Views/Search/SearchEverywhereView.swift index 97da329b5..fc2c63c5a 100644 --- a/macos/Sources/Lithe/Views/Search/SearchEverywhereView.swift +++ b/macos/Sources/Lithe/Views/Search/SearchEverywhereView.swift @@ -232,7 +232,7 @@ struct SearchEverywhereView: View { } } } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .contentShape(Rectangle()) } @@ -406,7 +406,7 @@ struct SearchEverywhereView: View { } .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() Button { @@ -414,7 +414,7 @@ struct SearchEverywhereView: View { } label: { LitheIcon(kind: .folder, size: 13) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help("Show in Finder") } @@ -490,7 +490,7 @@ struct SearchEverywhereView: View { .background(index == selectedIndex ? LitheTheme.selection : .clear) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } diff --git a/macos/Sources/Lithe/Views/Search/SearchSidebarView.swift b/macos/Sources/Lithe/Views/Search/SearchSidebarView.swift index 8b2b0169f..1d3d26ea2 100644 --- a/macos/Sources/Lithe/Views/Search/SearchSidebarView.swift +++ b/macos/Sources/Lithe/Views/Search/SearchSidebarView.swift @@ -112,7 +112,7 @@ struct SearchSidebarView: View { .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .litheContextMenu { [ diff --git a/macos/Sources/Lithe/Views/Terminal/TerminalView.swift b/macos/Sources/Lithe/Views/Terminal/TerminalView.swift index affdfb183..5c300a41e 100644 --- a/macos/Sources/Lithe/Views/Terminal/TerminalView.swift +++ b/macos/Sources/Lithe/Views/Terminal/TerminalView.swift @@ -178,7 +178,7 @@ struct TerminalView: View { .frame(width: 22, height: 22) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help("Close \(feature.terminalTitle(for: session))") } .background(isActive ? LitheTheme.subtleSelection : .clear) diff --git a/macos/Sources/Lithe/Views/Workbench/OutputTextView.swift b/macos/Sources/Lithe/Views/Workbench/OutputTextView.swift index f875e2d89..3bf0fbd93 100644 --- a/macos/Sources/Lithe/Views/Workbench/OutputTextView.swift +++ b/macos/Sources/Lithe/Views/Workbench/OutputTextView.swift @@ -328,7 +328,7 @@ struct OutputTextView: View { .clipShape(Capsule()) .overlay(Capsule().stroke(LitheTheme.panelBorder, lineWidth: 1)) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .foregroundStyle(LitheTheme.primaryText) .disabled(output.isEmpty) @@ -349,7 +349,7 @@ struct OutputTextView: View { .clipShape(Capsule()) .overlay(Capsule().stroke(LitheTheme.panelBorder, lineWidth: 1)) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .foregroundStyle(LitheTheme.primaryText) } diff --git a/macos/Sources/Lithe/Views/Workbench/ProjectPreparationStatusView.swift b/macos/Sources/Lithe/Views/Workbench/ProjectPreparationStatusView.swift index 16102fb8a..ea6b147d7 100644 --- a/macos/Sources/Lithe/Views/Workbench/ProjectPreparationStatusView.swift +++ b/macos/Sources/Lithe/Views/Workbench/ProjectPreparationStatusView.swift @@ -39,7 +39,7 @@ private struct ProjectPreparationContent: View { } .font(.system(size: 11)) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help(chinese ? "查看项目准备详情" : "View project preparation details") .popover(isPresented: $showingDetails, arrowEdge: compact ? .bottom : .top) { VStack(alignment: .leading, spacing: 12) { diff --git a/macos/Sources/Lithe/Views/Workbench/WorkbenchStatusViews.swift b/macos/Sources/Lithe/Views/Workbench/WorkbenchStatusViews.swift index 7c35c8537..ba2b4fda7 100644 --- a/macos/Sources/Lithe/Views/Workbench/WorkbenchStatusViews.swift +++ b/macos/Sources/Lithe/Views/Workbench/WorkbenchStatusViews.swift @@ -14,7 +14,7 @@ struct EditorCaretPositionLabel: View { Text(chrome.caret.map { "\($0.line + 1):\($0.utf16Column + 1)" } ?? "1:1") .monospacedDigit() } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help("Go to Line…") } @@ -39,7 +39,7 @@ struct MemoryUsageStatusView: View { Image(systemName: "memorychip") } } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help( Text( diff --git a/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift b/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift index 1539b04aa..82fc6b9f3 100644 --- a/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift +++ b/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift @@ -510,7 +510,7 @@ struct WorkbenchView: View { .font(.system(size: 10, weight: .semibold)) .foregroundStyle(LitheTheme.tertiaryText) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .frame(width: 16, height: 16) .contentShape(Rectangle()) .litheRowHover(cornerRadius: LitheTheme.Metrics.cornerRadius, animation: nil) @@ -656,7 +656,7 @@ struct WorkbenchView: View { .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .accessibilityIdentifier("project-tab-\(projectModel.id.uuidString)") @@ -732,7 +732,7 @@ struct WorkbenchView: View { activeBackground: LitheTheme.subtleSelection ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .accessibilityIdentifier("project-switcher-\(model.id.uuidString)") .anchorPreference( @@ -769,7 +769,7 @@ struct WorkbenchView: View { activeBackground: LitheTheme.subtleSelection ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .anchorPreference( key: BranchSwitcherButtonBoundsPreferenceKey.self, @@ -1046,7 +1046,7 @@ struct WorkbenchView: View { .frame(width: 28, height: 28) .litheRowHover(isActive: false, cornerRadius: 6, activeBackground: LitheTheme.subtleSelection) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help(model.runFeatureIfActive?.isSelectedConfigurationRunning == true ? "Rerun selected configuration" : "Run selected configuration") .accessibilityLabel(model.runFeatureIfActive?.isSelectedConfigurationRunning == true ? "Rerun selected configuration" : "Run selected configuration") @@ -1068,7 +1068,7 @@ struct WorkbenchView: View { .frame(width: 28, height: 28) .litheRowHover(isActive: false, cornerRadius: 6, activeBackground: LitheTheme.subtleSelection) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help(isDebugSessionActive ? "Rerun or show Debug session" : "Debug selected run configuration") .accessibilityLabel(isDebugSessionActive ? "Rerun or show Debug session" : "Debug selected run configuration") @@ -1092,7 +1092,7 @@ struct WorkbenchView: View { .frame(width: 28, height: 28) .litheRowHover(isActive: false, cornerRadius: 6, activeBackground: LitheTheme.subtleSelection) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help("Stop active execution") .accessibilityLabel("Stop active execution") @@ -1131,7 +1131,7 @@ struct WorkbenchView: View { .frame(height: 30) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .frame(minWidth: 160, maxWidth: 190, alignment: .leading) .frame(height: 30) .litheRowHover(isActive: false, cornerRadius: 6, activeBackground: LitheTheme.subtleSelection) @@ -1186,7 +1186,7 @@ struct WorkbenchView: View { .contentShape(Rectangle()) .litheRowHover(isActive: isSelected, cornerRadius: 6, activeBackground: LitheTheme.subtleSelection) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .help(configuration.name) .accessibilityAddTraits(isSelected ? .isSelected : []) } @@ -1212,7 +1212,7 @@ struct WorkbenchView: View { activeBackground: LitheTheme.subtleSelection ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .foregroundStyle(LitheTheme.secondaryText) .help("Change workbench background") @@ -1255,7 +1255,7 @@ struct WorkbenchView: View { .frame(width: ActivityBarMetrics.slotWidth, height: ActivityBarMetrics.slotHeight) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(!destination.isAvailable) .foregroundStyle(model.workbenchFeature.selectedSidebar == destination ? LitheTheme.toolWindowSelectedText : LitheTheme.toolWindowButtonText) .workbenchHoverHelp( @@ -1343,7 +1343,7 @@ struct WorkbenchView: View { .frame(width: ActivityBarMetrics.slotWidth, height: ActivityBarMetrics.slotHeight) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle( isNotificationCenterPresented ? LitheTheme.toolWindowSelectedText : unreadNotificationCount > 0 ? LitheTheme.primaryText @@ -1460,7 +1460,7 @@ struct WorkbenchView: View { .frame(width: ActivityBarMetrics.slotWidth, height: ActivityBarMetrics.slotHeight) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(isSelected ? LitheTheme.toolWindowSelectedText : LitheTheme.toolWindowButtonText) .workbenchHoverHelp(Text(LocalizedStringKey(help)), placement: tooltipPlacement) .accessibilityLabel(Text(LocalizedStringKey(help))) @@ -1695,7 +1695,7 @@ struct WorkbenchView: View { } .foregroundStyle(isEmphasized ? LitheTheme.primaryText : LitheTheme.secondaryText) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help(LocalizedStringKey(title)) } @@ -1839,7 +1839,7 @@ private struct WorkbenchNotificationCenterView: View { Button("Clear All") { model.clearNotifications() } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .font(.system(size: 11.5, weight: .medium)) .foregroundStyle( model.notifications.isEmpty @@ -2398,7 +2398,7 @@ struct WorkbenchBackgroundPicker: View { Button("Remove") { model.workbenchBackgroundFeature.clear() } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.accent) .lithePointer() } @@ -2455,7 +2455,7 @@ struct WorkbenchBackgroundPicker: View { } .frame(width: 100) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .accessibilityLabel(Text(LocalizedStringKey(preset.title))) } diff --git a/macos/Sources/Lithe/Views/Workspace/DependencySidebarView.swift b/macos/Sources/Lithe/Views/Workspace/DependencySidebarView.swift index b670a5619..c50099dc1 100644 --- a/macos/Sources/Lithe/Views/Workspace/DependencySidebarView.swift +++ b/macos/Sources/Lithe/Views/Workspace/DependencySidebarView.swift @@ -162,7 +162,7 @@ private struct DependencyServiceSection: View { .frame(maxWidth: .infinity, minHeight: 30, alignment: .leading) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() Button { @@ -173,7 +173,7 @@ private struct DependencyServiceSection: View { .foregroundStyle(LitheTheme.secondaryText) .frame(width: 28, height: 28) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .help("Configure dependency search paths") .accessibilityIdentifier("dependency-path-settings-\(service.id)") @@ -214,7 +214,7 @@ private struct DependencyServiceSection: View { .foregroundStyle(LitheTheme.secondaryText) .lineLimit(3) Button("Retry") { loadDependencies() } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .foregroundStyle(LitheTheme.accent) .lithePointer() } @@ -334,7 +334,7 @@ private struct DependencyTreeNodeView: View { .frame(maxWidth: .infinity, minHeight: 28, alignment: .leading) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() if isExpanded { @@ -366,7 +366,7 @@ private struct DependencyTreeNodeView: View { Group { if case .virtualDocument(let uri) = node.source { Button { onOpenVirtual(uri) } label: { pathRowContent } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } else { pathRowContent @@ -535,7 +535,7 @@ private struct DependencyPathConfigurationEditor: View { } label: { LitheSystemIcon(systemImage: "arrow.uturn.backward") } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .help("Restore path") } } @@ -609,7 +609,7 @@ private struct DependencyPathConfigurationEditor: View { } label: { LitheSystemIcon(systemImage: "minus.circle") } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .help("Remove path") } } @@ -763,14 +763,14 @@ private struct DependencyFolderBrowser: View { LitheSystemIcon(systemImage: "arrow.up") .frame(width: 24, height: 24) } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .disabled(folderURL.path == "/") .help("Parent folder") Button { folderURL = workspaceURL.standardizedFileURL } label: { LitheSystemIcon(systemImage: "house") .frame(width: 24, height: 24) } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .help("Workspace folder") Text(folderURL.path) .font(.system(size: 10.5, design: .monospaced)) @@ -878,7 +878,7 @@ private struct DependencyFolderBrowser: View { LitheSystemIcon(systemImage: "chevron.right") .frame(width: 24, height: 24) } - .buttonStyle(.borderless) + .buttonStyle(.litheNoPress) .help("Open folder") } } diff --git a/macos/Sources/Lithe/Views/Workspace/ProjectSidebarView.swift b/macos/Sources/Lithe/Views/Workspace/ProjectSidebarView.swift index 69778b677..aeea8f06f 100644 --- a/macos/Sources/Lithe/Views/Workspace/ProjectSidebarView.swift +++ b/macos/Sources/Lithe/Views/Workspace/ProjectSidebarView.swift @@ -499,7 +499,7 @@ private struct FileNodeRow: View { animation: nil ) } - .buttonStyle(LitheTreeRowButtonStyle()) + .buttonStyle(.litheNoPress) .lithePointer() .padding(.horizontal, LitheTheme.Metrics.projectTreeContentHorizontalInset) .litheContextMenu( @@ -547,7 +547,7 @@ private struct FileNodeRow: View { animation: nil ) } - .buttonStyle(LitheTreeRowButtonStyle()) + .buttonStyle(.litheNoPress) .lithePointer() .padding(.horizontal, LitheTheme.Metrics.projectTreeContentHorizontalInset) .litheContextMenu( diff --git a/macos/Sources/Lithe/Views/Workspace/ProjectSwitcherPopover.swift b/macos/Sources/Lithe/Views/Workspace/ProjectSwitcherPopover.swift index 1bac1ae49..ad4426f88 100644 --- a/macos/Sources/Lithe/Views/Workspace/ProjectSwitcherPopover.swift +++ b/macos/Sources/Lithe/Views/Workspace/ProjectSwitcherPopover.swift @@ -100,7 +100,7 @@ struct ProjectSwitcherPopover: View { .contentShape(Rectangle()) .litheRowHover(cornerRadius: 5) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() } @@ -118,7 +118,7 @@ struct ProjectSwitcherPopover: View { isCurrent: isCurrent ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .lithePointer() .litheRowHover( isActive: isCurrent, @@ -141,7 +141,7 @@ struct ProjectSwitcherPopover: View { isCurrent: false ) } - .buttonStyle(.plain) + .buttonStyle(.litheNoPress) .disabled(!exists) .lithePointer() .litheRowHover(cornerRadius: 5) From 35beb4e7598ac83ee7cb8921ea01d071061ce702 Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 13:24:01 +0800 Subject: [PATCH 10/19] style(macos): hide keymap gear menu indicator --- macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift index eb1cebbcb..2e09bed75 100644 --- a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift @@ -69,6 +69,7 @@ struct KeyboardShortcutSettingsView: View { .foregroundStyle(LitheTheme.secondaryText) } .menuStyle(.borderlessButton) + .menuIndicator(.hidden) .tint(LitheTheme.secondaryText) .frame(width: 26) .accessibilityLabel("Restore All Defaults") From 425d18d28c8964d9c85ce8f1709bc2a8385943df Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 13:34:13 +0800 Subject: [PATCH 11/19] fix(macos): expose selected settings option to accessibility --- .../Lithe/Views/Components/LitheSettingsControls.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift index ffddb4130..7e9ab56d1 100644 --- a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift +++ b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift @@ -140,6 +140,7 @@ struct LitheSettingsSelect: View { .buttonStyle(.litheNoPress) .lithePointer() .accessibilityLabel(Text(LocalizedStringKey(accessibilityLabel))) + .accessibilityValue(Text(LocalizedStringKey(title(selection)))) .onChange(of: options) { _ in if isPresented { showPopup() } } @@ -233,6 +234,7 @@ private final class LitheSettingsSelectAnchorNSView: NSView { @MainActor private final class LitheSettingsSelectPopupState: ObservableObject { + let selectedIndex: Int @Published var highlightedIndex: Int @Published var keyboardScrollIndex: Int? @Published var popupHeight: CGFloat = 0 @@ -240,6 +242,7 @@ private final class LitheSettingsSelectPopupState: ObservableObject { let onChoose: (Int) -> Void init(selectedIndex: Int, optionCount: Int, onChoose: @escaping (Int) -> Void) { + self.selectedIndex = selectedIndex highlightedIndex = selectedIndex self.optionCount = optionCount self.onChoose = onChoose @@ -322,6 +325,7 @@ private struct LitheSettingsSelectPopupContent: View { .contentShape(Rectangle()) } .buttonStyle(.litheNoPress) + .accessibilityAddTraits(state.selectedIndex == index ? .isSelected : []) .onHover { hovering in if hovering { state.highlightedIndex = index } } From 12b6e46f7ced7dbf1e7921cbc3931aaf90a566b8 Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 14:36:03 +0800 Subject: [PATCH 12/19] fix(macos): use shared context menus in keymap settings --- .../App/KeyboardShortcutSettingsView.swift | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift index 2e09bed75..27b8aa025 100644 --- a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift @@ -204,16 +204,19 @@ struct KeyboardShortcutSettingsView: View { .frame(height: 24) .background(selectedCommandID == command.id ? LitheTheme.settingsSelection : .clear) .clipShape(RoundedRectangle(cornerRadius: 4)) - .contextMenu { - Button("Add Shortcut") { - beginEditing(commandID: command.id, bindingIndex: nil) - } + .litheContextMenu { + var items: [LitheContextMenuItem] = [ + .action("Add Shortcut") { + beginEditing(commandID: command.id, bindingIndex: nil) + } + ] if feature.isCustomized(command.id) { - Button("Restore Default") { + items.append(.action("Restore Default") { cancelEditing() feature.resetCommand(command.id) - } + }) } + return items } if editingTarget?.commandID == command.id { @@ -282,10 +285,10 @@ struct KeyboardShortcutSettingsView: View { .lithePointer() .help("Edit Shortcut") .accessibilityLabel(binding.displayText) - .contextMenu { - Button("Remove Shortcut") { + .litheContextMenu { + [LitheContextMenuItem.action("Remove Shortcut") { removeBinding(commandID: commandID, index: index) - } + }] } } From 047edf83b671089f0bafb08b2c9eb7766386b45d Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 15:07:42 +0800 Subject: [PATCH 13/19] ci(windows): bound workspace commit integration test at 30 seconds --- .../write-stable-tests/references/windows-and-rust.md | 6 ++++-- .../write-stable-tests/scripts/test-stability-windows.ps1 | 5 +++-- .github/workflows/ci-windows.yml | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.agents/skills/write-stable-tests/references/windows-and-rust.md b/.agents/skills/write-stable-tests/references/windows-and-rust.md index ff2f1a1fa..b22335d59 100644 --- a/.agents/skills/write-stable-tests/references/windows-and-rust.md +++ b/.agents/skills/write-stable-tests/references/windows-and-rust.md @@ -52,8 +52,10 @@ Git subprocesses, sometimes rebuilding several repositories in one case. The SharedRust lane assigns the `tests::git_history_rewrite::` module and the older `tests::git::git_write_squashes_`, `git_write_deletes_a_local_commit_`, and `git_write_edits_a_local_commit_message_` scenarios a 30-second process budget. -Core rebase requests use a nested 20-second deadline. Other cases retain the -normal 15-second budget. The Rust runner's repeatable +Core rebase requests use a nested 20-second deadline. The workspace commit +submodule publication test also gets 30 seconds because it creates two local +remotes and runs multiple real Git pushes. Other cases retain the normal +15-second budget. The Rust runner's repeatable `--test-budget prefix=milliseconds` option uses the most specific matching prefix, records each case's effective budget in JSON, uses that budget for HTML/JUnit classification, and remains capped by the shared suite deadline. diff --git a/.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 b/.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 index 845a3dd94..ad320670b 100644 --- a/.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 +++ b/.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 @@ -69,10 +69,11 @@ function Invoke-TimedRustTests { $arguments += @("--package", $Package) } if ($Package -eq "lithe-core") { - # History-rewrite integration tests create and rewrite real repos, - # sometimes several per case. Keep unit tests at the normal limit. + # Integration tests that create multiple real repositories or push + # through Git subprocesses need a bounded allowance on Windows. foreach ($prefix in @( "tests::git_history_rewrite::", + "tests::git_workspace_commit::git_workspace_commit_push_checks_submodule_publication_before_updating_the_remote", "tests::git::git_write_squashes_", "tests::git::git_write_deletes_a_local_commit_", "tests::git::git_write_edits_a_local_commit_message_" diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 55d54b65b..1b9c6e8d9 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -460,6 +460,7 @@ jobs: --max-ms 15000 ` --test-budget lsp::interface::engine::real_jdt_tests::real_jdtls_discovers_builds_and_launches_java_25_entrypoints=600000 ` --test-budget tests::git_history_rewrite::=30000 ` + --test-budget tests::git_workspace_commit::git_workspace_commit_push_checks_submodule_publication_before_updating_the_remote=30000 ` --test-budget tests::git::git_write_squashes_=30000 ` --test-budget tests::git::git_write_deletes_a_local_commit_=30000 ` --test-budget tests::git::git_write_edits_a_local_commit_message_=30000 ` From 8d4022cf40fd08c99a0bcbb305b683536af8a9e9 Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 22:06:31 +0800 Subject: [PATCH 14/19] fix(macos): allow settings select trigger to close its popup --- docs/development/platform-parity-matrix.csv | 2 +- docs/development/platform-parity-matrix.md | 2 +- .../Components/LitheSettingsControls.swift | 24 +++++++++++-- .../SettingsSelectPopupGeometryTests.swift | 36 ++++++++++++++++++- shared/platform-feature-matrix.json | 2 +- 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv index a11a0562c..c02ee7049 100644 --- a/docs/development/platform-parity-matrix.csv +++ b/docs/development/platform-parity-matrix.csv @@ -73,7 +73,7 @@ outline-symbols,编辑器,代码结构,当前文件 Outline 与符号树,部分 references-pane,编辑器,代码结构,引用结果面板与引用跳转,已实现,待验证,已实现,待验证,Language Tooling,从符号发起引用查询,验证结果分组、文件定位、关闭和重新查询。,,macos/Sources/Lithe/Views/Language/JavaReferencesView.swift,windows/tauri/src/features/references diagnostics-workspace,可观测性,诊断与日志,工作区诊断与问题面板,已实现,待验证,已实现,待验证,Diagnostics,制造多个来源的错误和警告,验证筛选、排序、定位和清除。,,macos/Sources/Lithe/Application/Features/DiagnosticsFeatureModel.swift; macos/Sources/Lithe/Views/Language/JavaProblemsView.swift,windows/tauri/src/features/diagnostics; windows/tauri/src/features/logging; windows/tauri/src/ui/dialog.tsx diagnostics-export,可观测性,诊断与日志,诊断包导出与脱敏,已实现,待验证,已实现,待验证,Diagnostics,导出诊断包,确认敏感路径、凭据和大文件被正确脱敏或限制;检查预览说明和文件列表在确认弹窗内完整显示、长内容正常换行且不溢出。,,macos/Sources/Lithe/Views/Diagnostics/DiagnosticsExportSheet.swift; macos/Sources/Lithe/Services/Diagnostics,windows/tauri/src/features/diagnostics -application-settings,设置,应用配置,应用、项目与运行设置,已实现,待验证,已实现,待验证,Settings,修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift; macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift,windows/tauri/src/features/settings +application-settings,设置,应用配置,应用、项目与运行设置,已实现,待验证,已实现,待验证,Settings,修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;再次点击原下拉按钮应收起弹层,点击另一处下拉框应切换弹层,点击空白处应关闭弹层;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift; macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift,windows/tauri/src/features/settings keymap-customization,设置,应用配置,快捷键查看与自定义,已实现,待验证,已实现,待验证,Settings,macOS 从设置 → 快捷键打开分类树,检查初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、右侧紧凑键帽及工具栏编辑菜单;通过键帽点击编辑,通过工具栏或右键菜单新增、删除与恢复默认,并检查键盘辅助功能能访问这些操作;按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。,Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。,macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift; macos/Sources/Lithe/Models/Keymap,windows/tauri/src/features/keymaps; windows/tauri/src/features/settings/components/macos-settings-panels.tsx; windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx; windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx plugin-extension-management,扩展,插件与主题,插件、语言服务与主题管理,已实现,待验证,已实现,待验证,Extensions,启用、禁用和恢复插件/主题,确认启动加载、失败隔离和设置持久化。,,macos/Sources/Lithe/Views/App/PluginManagementView.swift; macos/Sources/Lithe/Platform/MacOS/Plugins,windows/tauri/src/extensions; windows/tauri/src/features/settings onboarding-welcome,启动与窗口,首次启动,欢迎页、项目入口与首次启动引导,已实现,待验证,已实现,待验证,Onboarding,清空首次启动状态,验证打开项目、克隆项目、最近项目和跳过引导。,,macos/Sources/Lithe/Views/App/WelcomeView.swift; macos/Sources/Lithe/Views/Workspace,windows/tauri/src/features/onboarding; windows/tauri/src/features/bootstrap diff --git a/docs/development/platform-parity-matrix.md b/docs/development/platform-parity-matrix.md index 8c7fbb04a..f1895bed2 100644 --- a/docs/development/platform-parity-matrix.md +++ b/docs/development/platform-parity-matrix.md @@ -211,7 +211,7 @@ | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | -| 应用配置 | **应用、项目与运行设置**
application-settings | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift`、`macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift`、`macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings` | Settings | 修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。 | | +| 应用配置 | **应用、项目与运行设置**
application-settings | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift`、`macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift`、`macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings` | Settings | 修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;再次点击原下拉按钮应收起弹层,点击另一处下拉框应切换弹层,点击空白处应关闭弹层;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。 | | | 应用配置 | **快捷键查看与自定义**
keymap-customization | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift`、`macos/Sources/Lithe/Models/Keymap` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/keymaps`、`windows/tauri/src/features/settings/components/macos-settings-panels.tsx`、`windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx`、`windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx` | Settings | macOS 从设置 → 快捷键打开分类树,检查初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、右侧紧凑键帽及工具栏编辑菜单;通过键帽点击编辑,通过工具栏或右键菜单新增、删除与恢复默认,并检查键盘辅助功能能访问这些操作;按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。 | Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。 | diff --git a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift index 7e9ab56d1..6b9deca99 100644 --- a/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift +++ b/macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift @@ -185,6 +185,7 @@ struct LitheSettingsSelect: View { ownerID: popupID, content: AnyView(popup), state: state, + anchorWindow: window, anchorFrame: anchorFrame, size: NSSize(width: popupWidth, height: popupHeight), visibleFrame: visibleFrame, @@ -339,6 +340,14 @@ private struct LitheSettingsSelectPopupContent: View { } struct LitheSettingsSelectPopupGeometry { + @MainActor + static func isAnchorClick(_ event: NSEvent?, anchorWindow: NSWindow?, anchorFrame: NSRect?) -> Bool { + guard let event, event.type == .leftMouseDown, + let anchorWindow, let anchorFrame, + event.windowNumber == anchorWindow.windowNumber else { return false } + return anchorFrame.contains(anchorWindow.convertPoint(toScreen: event.locationInWindow)) + } + static func frame(anchor: NSRect, size: NSSize, visibleFrame: NSRect) -> NSRect { let bounds = visibleFrame.insetBy(dx: 6, dy: 6) let width = min(size.width, bounds.width) @@ -374,6 +383,8 @@ private final class LitheSettingsSelectPopupPresenter: NSObject, NSWindowDelegat static let shared = LitheSettingsSelectPopupPresenter() private var panel: LitheSettingsSelectPopupPanel? + private weak var anchorWindow: NSWindow? + private var anchorFrame: NSRect? private var ownerID: UUID? private var onDismiss: (() -> Void)? private var localEventMonitor: Any? @@ -383,6 +394,7 @@ private final class LitheSettingsSelectPopupPresenter: NSObject, NSWindowDelegat ownerID: UUID, content: AnyView, state: LitheSettingsSelectPopupState, + anchorWindow: NSWindow, anchorFrame: NSRect, size: NSSize, visibleFrame: NSRect, @@ -415,6 +427,8 @@ private final class LitheSettingsSelectPopupPresenter: NSObject, NSWindowDelegat panel.collectionBehavior = [.transient, .fullScreenAuxiliary] panel.delegate = self self.panel = panel + self.anchorWindow = anchorWindow + self.anchorFrame = anchorFrame self.ownerID = ownerID self.onDismiss = onDismiss installEventMonitors() @@ -428,6 +442,8 @@ private final class LitheSettingsSelectPopupPresenter: NSObject, NSWindowDelegat let callback = onDismiss onDismiss = nil self.ownerID = nil + anchorWindow = nil + anchorFrame = nil let closingPanel = panel panel = nil closingPanel?.delegate = nil @@ -437,13 +453,17 @@ private final class LitheSettingsSelectPopupPresenter: NSObject, NSWindowDelegat } func windowDidResignKey(_ notification: Notification) { - dismiss() + if !LitheSettingsSelectPopupGeometry.isAnchorClick( + NSApp.currentEvent, anchorWindow: anchorWindow, anchorFrame: anchorFrame + ) { dismiss() } } private func installEventMonitors() { localEventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] event in guard let self else { return event } - if event.window !== self.panel { self.dismiss() } + if event.window !== self.panel && !LitheSettingsSelectPopupGeometry.isAnchorClick( + event, anchorWindow: self.anchorWindow, anchorFrame: self.anchorFrame + ) { self.dismiss() } return event } globalEventMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] _ in diff --git a/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift b/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift index f7f960910..2c4c7068f 100644 --- a/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift +++ b/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift @@ -1,9 +1,43 @@ -import CoreGraphics +import AppKit import Testing @testable import Lithe @Suite("Settings select popup geometry") struct SettingsSelectPopupGeometryTests { + @MainActor + @Test + func onlyLeftClickOnOriginalControlDefersDismissal() throws { + let window = NSWindow( + contentRect: NSRect(x: 100, y: 100, width: 400, height: 300), + styleMask: [.borderless], backing: .buffered, defer: false + ) + window.isReleasedWhenClosed = false + defer { window.close() } + let anchor = window.convertToScreen(NSRect(x: 20, y: 200, width: 190, height: 28)) + + func click(at point: NSPoint, type: NSEvent.EventType = .leftMouseDown) throws -> NSEvent { + try #require(NSEvent.mouseEvent( + with: type, location: point, modifierFlags: [], timestamp: 0, + windowNumber: window.windowNumber, context: nil, eventNumber: 1, + clickCount: 1, pressure: 1 + )) + } + + #expect(LitheSettingsSelectPopupGeometry.isAnchorClick( + try click(at: NSPoint(x: 100, y: 214)), anchorWindow: window, anchorFrame: anchor + )) + #expect(!LitheSettingsSelectPopupGeometry.isAnchorClick( + try click(at: NSPoint(x: 260, y: 214)), anchorWindow: window, anchorFrame: anchor + )) + #expect(!LitheSettingsSelectPopupGeometry.isAnchorClick( + try click(at: NSPoint(x: 300, y: 50)), anchorWindow: window, anchorFrame: anchor + )) + #expect(!LitheSettingsSelectPopupGeometry.isAnchorClick( + try click(at: NSPoint(x: 100, y: 214), type: .rightMouseDown), + anchorWindow: window, anchorFrame: anchor + )) + } + @Test func opensBelowTheControlWithoutAnArrowGap() { let anchor = CGRect(x: 100, y: 500, width: 190, height: 28) diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index 1d5e57c2c..c1cf3889f 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -1875,7 +1875,7 @@ "verificationStatus": "pending" }, "owner": "Settings", - "verification": "修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。" + "verification": "修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;再次点击原下拉按钮应收起弹层,点击另一处下拉框应切换弹层,点击空白处应关闭弹层;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;保持窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。" }, { "id": "keymap-customization", From 7642552c58ab18b386850fd8dab625452a5b494b Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 23:05:41 +0800 Subject: [PATCH 15/19] test(macos): cover settings select popup click sequence --- .../SettingsSelectPopupGeometryTests.swift | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift b/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift index 2c4c7068f..e797b1b44 100644 --- a/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift +++ b/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift @@ -1,9 +1,80 @@ import AppKit +import SwiftUI import Testing @testable import Lithe -@Suite("Settings select popup geometry") +@Suite("Settings select popup") struct SettingsSelectPopupGeometryTests { + @MainActor + @Test + func popupClosesOnItsTriggerAndBlankSpaceButSwitchesToAnotherSelect() async throws { + let host = NSHostingView(rootView: HStack(spacing: 20) { + LitheSettingsSelect( + selection: .constant("First"), options: ["First", "Another"], + width: 180, accessibilityLabel: "First select", title: { $0 } + ) + LitheSettingsSelect( + selection: .constant("Second"), options: ["Second", "Another"], + width: 180, accessibilityLabel: "Second select", title: { $0 } + ) + Spacer() + }.frame(width: 450, height: 120, alignment: .topLeading)) + let window = NSWindow( + contentRect: NSRect(x: 100, y: 100, width: 450, height: 120), + styleMask: [.borderless], backing: .buffered, defer: false + ) + window.isReleasedWhenClosed = false + window.contentView = host + window.makeKeyAndOrderFront(nil) + host.layoutSubtreeIfNeeded() + func popup() -> NSPanel? { + NSApp.windows.compactMap { $0 as? NSPanel }.first { + $0.isVisible && NSStringFromClass(type(of: $0)).hasSuffix("LitheSettingsSelectPopupPanel") + } + } + + func click(_ point: NSPoint) throws { + for type in [NSEvent.EventType.leftMouseDown, .leftMouseUp] { + let event = try #require(NSEvent.mouseEvent( + with: type, location: host.convert(point, to: nil), modifierFlags: [], + timestamp: 0, windowNumber: window.windowNumber, context: nil, + eventNumber: 1, clickCount: 1, pressure: type == .leftMouseDown ? 1 : 0 + )) + NSApp.sendEvent(event) + } + } + defer { + if popup() != nil { try? click(NSPoint(x: 420, y: 80)) } + window.orderOut(nil) + window.close() + } + + func waitForPopup(_ condition: @escaping (NSPanel?) -> Bool) async -> Bool { + let clock = ContinuousClock() + let deadline = clock.now.advanced(by: .seconds(1)) + while clock.now < deadline { + if condition(popup()) { return true } + await Task.yield() + } + return condition(popup()) + } + + try click(NSPoint(x: 80, y: 14)) + #expect(await waitForPopup { $0 != nil }) + let firstFrame = try #require(popup()?.frame) + try click(NSPoint(x: 80, y: 14)) + #expect(await waitForPopup { $0 == nil }, "Clicking the open select must close it") + + try click(NSPoint(x: 80, y: 14)) + #expect(await waitForPopup { $0 != nil }) + try click(NSPoint(x: 280, y: 14)) + #expect(await waitForPopup { ($0?.frame.minX ?? 0) > firstFrame.minX + 100 }, + "The second select must replace the first popup") + + try click(NSPoint(x: 420, y: 80)) + #expect(await waitForPopup { $0 == nil }, "Blank space must close the popup") + } + @MainActor @Test func onlyLeftClickOnOriginalControlDefersDismissal() throws { From 57e8eaa0344f2880b569ccde5b5d65ad4e09e122 Mon Sep 17 00:00:00 2001 From: Xiaoyumuxi <3075514079@qq.com> Date: Tue, 29 Sep 2026 07:50:18 +0800 Subject: [PATCH 16/19] fix(macos): label keymap toolbar controls for accessibility --- .../Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift index 179222acb..c2e275661 100644 --- a/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift +++ b/macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift @@ -98,6 +98,7 @@ struct KeyboardShortcutSettingsView: View { LitheIDEAIcon(resourcePath: "expui/general/expandAll.svg", size: 16, fallbackSystemImage: "arrow.down.right.and.arrow.up.left", preservesOriginalColors: true) } .help("Expand All") + .accessibilityLabel("Expand All") Button { expandedGroups = [] @@ -106,6 +107,7 @@ struct KeyboardShortcutSettingsView: View { LitheIDEAIcon(resourcePath: "expui/general/collapseAll.svg", size: 16, fallbackSystemImage: "arrow.up.left.and.arrow.down.right", preservesOriginalColors: true) } .help("Collapse All") + .accessibilityLabel("Collapse All") Menu { if let selectedCommandID, @@ -133,6 +135,7 @@ struct KeyboardShortcutSettingsView: View { .tint(LitheTheme.secondaryText) .disabled(selectedCommandID == nil) .help("Edit Shortcuts") + .accessibilityLabel("Edit Shortcuts") Spacer(minLength: 12) LitheSettingsSearchField("", text: $query) { _ in From ad799b4112f76c70622b42cade402dd30395aa2a Mon Sep 17 00:00:00 2001 From: Xiaoyumuxi <3075514079@qq.com> Date: Tue, 29 Sep 2026 07:50:24 +0800 Subject: [PATCH 17/19] docs: remove duplicate settings verification steps --- shared/platform-feature-matrix.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index efd44eccf..cd92ecf38 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -1914,7 +1914,7 @@ "verificationStatus": "pending" }, "owner": "Settings", - "verification": "修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 保持设置窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏、设置树、插件列表与详情背景和主界面侧边栏使用同一主题色,窗口按钮可用且可拖动;缩小设置窗口确认最小尺寸 900×700,底部操作按钮为 72×28 且边框与输入框一样以 1pt 内描边绘制;拖动设置树与插件列表右侧分隔线,确认设置分类栏不能小于原始宽度、插件列表分隔线可向左缩至 200 pt 或向右加宽,在 900 pt 最小宽度下仍有至少 150 pt 可分配的拖动余量,窗口缩小时设置内容区保留至少 500 pt、插件详情区至少 160 pt,拖动结束后宽度可恢复,悬停及拖动分隔线仅显示左右调整光标而不改变线条颜色;点击设置搜索框后切换分类及点击内容空白区域,确认焦点边框消失;悬停插件条目及设置窗口各子页面的按钮和选择控件时光标保持箭头;从主窗口打开设置后切换到其他应用,确认设置窗口随主窗口置后且不置顶遮挡;在插件页点击 Enable 后点击设置窗口 OK,确认成功时应用并关闭、失败时保留待确认状态且可重试;应用期间确认窗口关闭按钮和常规关闭命令不能关闭窗口,应用结束后恢复关闭能力;A 项目开始应用插件后从 B 项目重新打开共享设置窗口,确认 A 完成后 B 窗口仍打开且未应用草稿保留;待确认时点击 Cancel 或窗口关闭按钮,确认重开后草稿已清除;亮色模式确认分段选择的选中文字清晰可读;检查搜索框、普通及密码输入框、多行编辑框(含获得与失去焦点的边框)、数值步进器、下拉框和分段选择在各设置子页面中的背景、边框、焦点态及操作行为一致,关闭后重开再检查;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;再次点击原下拉按钮应收起弹层,点击另一处下拉框应切换弹层,点击空白处应关闭弹层;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;再次点击原下拉按钮应收起弹层,点击另一处下拉框应切换弹层,点击空白处应关闭弹层;缩小可用屏幕高度时检查弹层不遮挡控件。" + "verification": "修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 保持设置窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏、设置树、插件列表与详情背景和主界面侧边栏使用同一主题色,窗口按钮可用且可拖动;缩小设置窗口确认最小尺寸 900×700,底部操作按钮为 72×28 且边框与输入框一样以 1pt 内描边绘制;拖动设置树与插件列表右侧分隔线,确认设置分类栏不能小于原始宽度、插件列表分隔线可向左缩至 200 pt 或向右加宽,在 900 pt 最小宽度下仍有至少 150 pt 可分配的拖动余量,窗口缩小时设置内容区保留至少 500 pt、插件详情区至少 160 pt,拖动结束后宽度可恢复,悬停及拖动分隔线仅显示左右调整光标而不改变线条颜色;点击设置搜索框后切换分类及点击内容空白区域,确认焦点边框消失;悬停插件条目及设置窗口各子页面的按钮和选择控件时光标保持箭头;从主窗口打开设置后切换到其他应用,确认设置窗口随主窗口置后且不置顶遮挡;在插件页点击 Enable 后点击设置窗口 OK,确认成功时应用并关闭、失败时保留待确认状态且可重试;应用期间确认窗口关闭按钮和常规关闭命令不能关闭窗口,应用结束后恢复关闭能力;A 项目开始应用插件后从 B 项目重新打开共享设置窗口,确认 A 完成后 B 窗口仍打开且未应用草稿保留;待确认时点击 Cancel 或窗口关闭按钮,确认重开后草稿已清除;亮色模式确认分段选择的选中文字清晰可读;检查搜索框、普通及密码输入框、多行编辑框(含获得与失去焦点的边框)、数值步进器、下拉框和分段选择在各设置子页面中的背景、边框、焦点态及操作行为一致,关闭后重开再检查;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;再次点击原下拉按钮应收起弹层,点击另一处下拉框应切换弹层,点击空白处应关闭弹层;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态。" }, { "id": "keymap-customization", From dde2054428e02789e3775d90096ece61ba684f93 Mon Sep 17 00:00:00 2001 From: Xiaoyumuxi <3075514079@qq.com> Date: Tue, 29 Sep 2026 07:50:28 +0800 Subject: [PATCH 18/19] docs: remove duplicate settings verification steps --- docs/development/platform-parity-matrix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/platform-parity-matrix.md b/docs/development/platform-parity-matrix.md index 03f5848bb..71a87c461 100644 --- a/docs/development/platform-parity-matrix.md +++ b/docs/development/platform-parity-matrix.md @@ -212,7 +212,7 @@ | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | -| 应用配置 | **应用、项目与运行设置**
application-settings | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Views/App/RootView.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Views/App/PluginManagementView.swift`、`macos/Sources/Lithe/Models/Workspace/ProjectSessionManager.swift`、`macos/Sources/Lithe/Models/AppModel/AppModel+PluginManagement.swift`、`macos/Sources/Lithe/Views/Components/LitheSplitPaneView.swift`、`macos/Sources/Lithe/Views/Workbench/SplitHandleView.swift`、`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift`、`macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift`、`macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift`、`macos/Sources/Lithe/Theme/LitheTheme.swift`、`macos/Tests/LitheTests/PluginManagementPresentationTests.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings` | Settings | 修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 保持设置窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏、设置树、插件列表与详情背景和主界面侧边栏使用同一主题色,窗口按钮可用且可拖动;缩小设置窗口确认最小尺寸 900×700,底部操作按钮为 72×28 且边框与输入框一样以 1pt 内描边绘制;拖动设置树与插件列表右侧分隔线,确认设置分类栏不能小于原始宽度、插件列表分隔线可向左缩至 200 pt 或向右加宽,在 900 pt 最小宽度下仍有至少 150 pt 可分配的拖动余量,窗口缩小时设置内容区保留至少 500 pt、插件详情区至少 160 pt,拖动结束后宽度可恢复,悬停及拖动分隔线仅显示左右调整光标而不改变线条颜色;点击设置搜索框后切换分类及点击内容空白区域,确认焦点边框消失;悬停插件条目及设置窗口各子页面的按钮和选择控件时光标保持箭头;从主窗口打开设置后切换到其他应用,确认设置窗口随主窗口置后且不置顶遮挡;在插件页点击 Enable 后点击设置窗口 OK,确认成功时应用并关闭、失败时保留待确认状态且可重试;应用期间确认窗口关闭按钮和常规关闭命令不能关闭窗口,应用结束后恢复关闭能力;A 项目开始应用插件后从 B 项目重新打开共享设置窗口,确认 A 完成后 B 窗口仍打开且未应用草稿保留;待确认时点击 Cancel 或窗口关闭按钮,确认重开后草稿已清除;亮色模式确认分段选择的选中文字清晰可读;检查搜索框、普通及密码输入框、多行编辑框(含获得与失去焦点的边框)、数值步进器、下拉框和分段选择在各设置子页面中的背景、边框、焦点态及操作行为一致,关闭后重开再检查;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;再次点击原下拉按钮应收起弹层,点击另一处下拉框应切换弹层,点击空白处应关闭弹层;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;再次点击原下拉按钮应收起弹层,点击另一处下拉框应切换弹层,点击空白处应关闭弹层;缩小可用屏幕高度时检查弹层不遮挡控件。 | | +| 应用配置 | **应用、项目与运行设置**
application-settings | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Views/App/RootView.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Views/App/PluginManagementView.swift`、`macos/Sources/Lithe/Models/Workspace/ProjectSessionManager.swift`、`macos/Sources/Lithe/Models/AppModel/AppModel+PluginManagement.swift`、`macos/Sources/Lithe/Views/Components/LitheSplitPaneView.swift`、`macos/Sources/Lithe/Views/Workbench/SplitHandleView.swift`、`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift`、`macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift`、`macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift`、`macos/Sources/Lithe/Theme/LitheTheme.swift`、`macos/Tests/LitheTests/PluginManagementPresentationTests.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings` | Settings | 修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 保持设置窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏、设置树、插件列表与详情背景和主界面侧边栏使用同一主题色,窗口按钮可用且可拖动;缩小设置窗口确认最小尺寸 900×700,底部操作按钮为 72×28 且边框与输入框一样以 1pt 内描边绘制;拖动设置树与插件列表右侧分隔线,确认设置分类栏不能小于原始宽度、插件列表分隔线可向左缩至 200 pt 或向右加宽,在 900 pt 最小宽度下仍有至少 150 pt 可分配的拖动余量,窗口缩小时设置内容区保留至少 500 pt、插件详情区至少 160 pt,拖动结束后宽度可恢复,悬停及拖动分隔线仅显示左右调整光标而不改变线条颜色;点击设置搜索框后切换分类及点击内容空白区域,确认焦点边框消失;悬停插件条目及设置窗口各子页面的按钮和选择控件时光标保持箭头;从主窗口打开设置后切换到其他应用,确认设置窗口随主窗口置后且不置顶遮挡;在插件页点击 Enable 后点击设置窗口 OK,确认成功时应用并关闭、失败时保留待确认状态且可重试;应用期间确认窗口关闭按钮和常规关闭命令不能关闭窗口,应用结束后恢复关闭能力;A 项目开始应用插件后从 B 项目重新打开共享设置窗口,确认 A 完成后 B 窗口仍打开且未应用草稿保留;待确认时点击 Cancel 或窗口关闭按钮,确认重开后草稿已清除;亮色模式确认分段选择的选中文字清晰可读;检查搜索框、普通及密码输入框、多行编辑框(含获得与失去焦点的边框)、数值步进器、下拉框和分段选择在各设置子页面中的背景、边框、焦点态及操作行为一致,关闭后重开再检查;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;再次点击原下拉按钮应收起弹层,点击另一处下拉框应切换弹层,点击空白处应关闭弹层;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态。 | | | 应用配置 | **快捷键查看与自定义**
keymap-customization | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift`、`macos/Sources/Lithe/Models/Keymap` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/keymaps`、`windows/tauri/src/features/settings/components/macos-settings-panels.tsx`、`windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx`、`windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx` | Settings | macOS 从设置 → 快捷键依次选择 macOS、IntelliJ IDEA Classic、Eclipse,核对运行、调试、查找等操作在界面和实际监听器中的绑定;IDEA Classic 的 Stop Run 应为 Control-F2,Eclipse 的 Resume 应为 F8,继承父 keymap 的命令应使用上游绑定,Find、Replace、GotoImplementation 等显式清空的命令应无绑定;检查分类树初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、右侧紧凑键帽及工具栏编辑菜单;通过键帽、工具栏或右键菜单新增、删除与恢复默认,并检查键盘辅助功能;分别修改两套预设的同一命令,切换与重启后确认自定义绑定互不覆盖,并验证旧版单套覆盖迁移到 macOS;按命令名称、ID 和按键搜索,制造冲突并恢复当前预设或应用全部默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。Windows 保持现有快捷键编辑、导出和导入验证。 | Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。 | From 2610c53f629544259f468c42456eed4e91210492 Mon Sep 17 00:00:00 2001 From: Xiaoyumuxi <3075514079@qq.com> Date: Tue, 29 Sep 2026 07:50:31 +0800 Subject: [PATCH 19/19] docs: remove duplicate settings verification steps --- docs/development/platform-parity-matrix.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv index e96a336de..c6fb45708 100644 --- a/docs/development/platform-parity-matrix.csv +++ b/docs/development/platform-parity-matrix.csv @@ -74,7 +74,7 @@ outline-symbols,编辑器,代码结构,当前文件 Outline 与符号树,部分 references-pane,编辑器,代码结构,引用结果面板与引用跳转,已实现,待验证,已实现,待验证,Language Tooling,从符号发起引用查询,验证结果分组、文件定位、关闭和重新查询。,,macos/Sources/Lithe/Views/Language/JavaReferencesView.swift,windows/tauri/src/features/references diagnostics-workspace,可观测性,诊断与日志,工作区诊断与问题面板,已实现,待验证,已实现,待验证,Diagnostics,制造多个来源的错误和警告,验证筛选、排序、定位和清除。,,macos/Sources/Lithe/Application/Features/DiagnosticsFeatureModel.swift; macos/Sources/Lithe/Views/Language/JavaProblemsView.swift,windows/tauri/src/features/diagnostics; windows/tauri/src/features/logging; windows/tauri/src/ui/dialog.tsx diagnostics-export,可观测性,诊断与日志,诊断包导出与脱敏,已实现,待验证,已实现,待验证,Diagnostics,导出诊断包,确认敏感路径、凭据和大文件被正确脱敏或限制;检查预览说明和文件列表在确认弹窗内完整显示、长内容正常换行且不溢出。,,macos/Sources/Lithe/Views/Diagnostics/DiagnosticsExportSheet.swift; macos/Sources/Lithe/Services/Diagnostics,windows/tauri/src/features/diagnostics -application-settings,设置,应用配置,应用、项目与运行设置,已实现,待验证,已实现,待验证,Settings,修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 保持设置窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏、设置树、插件列表与详情背景和主界面侧边栏使用同一主题色,窗口按钮可用且可拖动;缩小设置窗口确认最小尺寸 900×700,底部操作按钮为 72×28 且边框与输入框一样以 1pt 内描边绘制;拖动设置树与插件列表右侧分隔线,确认设置分类栏不能小于原始宽度、插件列表分隔线可向左缩至 200 pt 或向右加宽,在 900 pt 最小宽度下仍有至少 150 pt 可分配的拖动余量,窗口缩小时设置内容区保留至少 500 pt、插件详情区至少 160 pt,拖动结束后宽度可恢复,悬停及拖动分隔线仅显示左右调整光标而不改变线条颜色;点击设置搜索框后切换分类及点击内容空白区域,确认焦点边框消失;悬停插件条目及设置窗口各子页面的按钮和选择控件时光标保持箭头;从主窗口打开设置后切换到其他应用,确认设置窗口随主窗口置后且不置顶遮挡;在插件页点击 Enable 后点击设置窗口 OK,确认成功时应用并关闭、失败时保留待确认状态且可重试;应用期间确认窗口关闭按钮和常规关闭命令不能关闭窗口,应用结束后恢复关闭能力;A 项目开始应用插件后从 B 项目重新打开共享设置窗口,确认 A 完成后 B 窗口仍打开且未应用草稿保留;待确认时点击 Cancel 或窗口关闭按钮,确认重开后草稿已清除;亮色模式确认分段选择的选中文字清晰可读;检查搜索框、普通及密码输入框、多行编辑框(含获得与失去焦点的边框)、数值步进器、下拉框和分段选择在各设置子页面中的背景、边框、焦点态及操作行为一致,关闭后重开再检查;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;再次点击原下拉按钮应收起弹层,点击另一处下拉框应切换弹层,点击空白处应关闭弹层;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态;再次点击原下拉按钮应收起弹层,点击另一处下拉框应切换弹层,点击空白处应关闭弹层;缩小可用屏幕高度时检查弹层不遮挡控件。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/RootView.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Views/App/PluginManagementView.swift; macos/Sources/Lithe/Models/Workspace/ProjectSessionManager.swift; macos/Sources/Lithe/Models/AppModel/AppModel+PluginManagement.swift; macos/Sources/Lithe/Views/Components/LitheSplitPaneView.swift; macos/Sources/Lithe/Views/Workbench/SplitHandleView.swift; macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift; macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift; macos/Sources/Lithe/Theme/LitheTheme.swift; macos/Tests/LitheTests/PluginManagementPresentationTests.swift,windows/tauri/src/features/settings +application-settings,设置,应用配置,应用、项目与运行设置,已实现,待验证,已实现,待验证,Settings,修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 保持设置窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏、设置树、插件列表与详情背景和主界面侧边栏使用同一主题色,窗口按钮可用且可拖动;缩小设置窗口确认最小尺寸 900×700,底部操作按钮为 72×28 且边框与输入框一样以 1pt 内描边绘制;拖动设置树与插件列表右侧分隔线,确认设置分类栏不能小于原始宽度、插件列表分隔线可向左缩至 200 pt 或向右加宽,在 900 pt 最小宽度下仍有至少 150 pt 可分配的拖动余量,窗口缩小时设置内容区保留至少 500 pt、插件详情区至少 160 pt,拖动结束后宽度可恢复,悬停及拖动分隔线仅显示左右调整光标而不改变线条颜色;点击设置搜索框后切换分类及点击内容空白区域,确认焦点边框消失;悬停插件条目及设置窗口各子页面的按钮和选择控件时光标保持箭头;从主窗口打开设置后切换到其他应用,确认设置窗口随主窗口置后且不置顶遮挡;在插件页点击 Enable 后点击设置窗口 OK,确认成功时应用并关闭、失败时保留待确认状态且可重试;应用期间确认窗口关闭按钮和常规关闭命令不能关闭窗口,应用结束后恢复关闭能力;A 项目开始应用插件后从 B 项目重新打开共享设置窗口,确认 A 完成后 B 窗口仍打开且未应用草稿保留;待确认时点击 Cancel 或窗口关闭按钮,确认重开后草稿已清除;亮色模式确认分段选择的选中文字清晰可读;检查搜索框、普通及密码输入框、多行编辑框(含获得与失去焦点的边框)、数值步进器、下拉框和分段选择在各设置子页面中的背景、边框、焦点态及操作行为一致,关闭后重开再检查;macOS 切换通用、编辑器、快捷键、项目、运行配置和语言服务等设置页,检查右侧路径标题、分组线、行对齐、说明文字、下拉框、复选框、分段控件与底部操作区;打开短列表和超过十项的下拉框,检查当前项蓝色高亮、无勾号、圆角、紧凑行距、鼠标悬停不跳动、键盘选择与按需滚动;再次点击原下拉按钮应收起弹层,点击另一处下拉框应切换弹层,点击空白处应关闭弹层;缩小可用屏幕高度时检查弹层不遮挡控件;使用辅助功能检查勾选框和分段控件的当前状态。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/RootView.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Views/App/PluginManagementView.swift; macos/Sources/Lithe/Models/Workspace/ProjectSessionManager.swift; macos/Sources/Lithe/Models/AppModel/AppModel+PluginManagement.swift; macos/Sources/Lithe/Views/Components/LitheSplitPaneView.swift; macos/Sources/Lithe/Views/Workbench/SplitHandleView.swift; macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift; macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift; macos/Sources/Lithe/Theme/LitheTheme.swift; macos/Tests/LitheTests/PluginManagementPresentationTests.swift,windows/tauri/src/features/settings keymap-customization,设置,应用配置,快捷键查看与自定义,已实现,待验证,已实现,待验证,Settings,macOS 从设置 → 快捷键依次选择 macOS、IntelliJ IDEA Classic、Eclipse,核对运行、调试、查找等操作在界面和实际监听器中的绑定;IDEA Classic 的 Stop Run 应为 Control-F2,Eclipse 的 Resume 应为 F8,继承父 keymap 的命令应使用上游绑定,Find、Replace、GotoImplementation 等显式清空的命令应无绑定;检查分类树初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、右侧紧凑键帽及工具栏编辑菜单;通过键帽、工具栏或右键菜单新增、删除与恢复默认,并检查键盘辅助功能;分别修改两套预设的同一命令,切换与重启后确认自定义绑定互不覆盖,并验证旧版单套覆盖迁移到 macOS;按命令名称、ID 和按键搜索,制造冲突并恢复当前预设或应用全部默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。Windows 保持现有快捷键编辑、导出和导入验证。,Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。,macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift; macos/Sources/Lithe/Models/Keymap,windows/tauri/src/features/keymaps; windows/tauri/src/features/settings/components/macos-settings-panels.tsx; windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx; windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx plugin-extension-management,扩展,插件与主题,插件、语言服务与主题管理,已实现,待验证,已实现,待验证,Extensions,macOS 全新安装时打开插件页,确认显示 PHP Support 未安装占位项,并可选择已签名的 PHP 插件包安装;选择非 PHP 包应被拒绝,安装后可搜索并启用、禁用 PHP,确认状态与重启恢复,且页面不显示无恢复路径的卸载操作;同时打开 Maven 面板确认插件内容不被遮挡。Windows 检查扩展管理入口和持久化。,,macos/Sources/Lithe/Views/App/PluginManagementView.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Models/AppModel/AppModel+PluginManagement.swift; macos/Tests/LitheTests/PluginManagementPresentationTests.swift; macos/Sources/Lithe/Platform/MacOS/Plugins,windows/tauri/src/extensions; windows/tauri/src/features/settings onboarding-welcome,启动与窗口,首次启动,欢迎页、项目入口与首次启动引导,已实现,待验证,已实现,待验证,Onboarding,清空首次启动状态,验证打开项目、克隆项目、最近项目和跳过引导;在 macOS 深色与浅色模式检查欢迎页布局、颜色和交互,确认默认及最小内容尺寸均为 800×650、可向屏幕可用区域放大,左下角齿轮菜单采用设置页下拉框配色并可进入设置与检查更新。,,macos/Sources/Lithe/Views/App/WelcomeView.swift; macos/Sources/Lithe/Views/Workspace,windows/tauri/src/features/onboarding; windows/tauri/src/features/bootstrap