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 0baa2c91a..06f737e12 100644
--- a/.agents/skills/write-stable-tests/references/windows-and-rust.md
+++ b/.agents/skills/write-stable-tests/references/windows-and-rust.md
@@ -56,7 +56,9 @@ The `tests::git_patch_exchange::patch_metadata_can_list_an_oversized_export_befo
case also gets 30 seconds because it writes and inspects a real file larger
than the 32 MiB patch limit; Windows CI terminated it at 16.1 seconds under
the default 15-second limit. Core rebase requests use a nested 20-second
-deadline. Other cases retain the normal 15-second budget. The Rust runner's repeatable
+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 04f656c82..4538d5f7a 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 accd12f58..16875d359 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 `
diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv
index 3d9aa3c19..c6fb45708 100644
--- a/docs/development/platform-parity-matrix.csv
+++ b/docs/development/platform-parity-matrix.csv
@@ -74,8 +74,8 @@ 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/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/Sources/Lithe/Theme/LitheTheme.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.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 和按键搜索,制造冲突并恢复当前预设或应用全部默认。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
+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
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 95384ad98..71a87c461 100644
--- a/docs/development/platform-parity-matrix.md
+++ b/docs/development/platform-parity-matrix.md
@@ -212,8 +212,8 @@
| 功能组 | 能力点 | 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/Sources/Lithe/Theme/LitheTheme.swift`、`macos/Tests/LitheTests/SettingsAppearanceContainerTests.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 或窗口关闭按钮,确认重开后草稿已清除;亮色模式确认分段选择的选中文字清晰可读;检查搜索框、普通及密码输入框、多行编辑框(含获得与失去焦点的边框)、数值步进器、下拉框和分段选择在各设置子页面中的背景、边框、焦点态及操作行为一致,关闭后重开再检查。 | |
-| 应用配置 | **快捷键查看与自定义**
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 和按键搜索,制造冲突并恢复当前预设或应用全部默认。Windows 保持现有快捷键编辑、导出和导入验证。 | Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。 |
+| 应用配置 | **应用、项目与运行设置**
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、原生文件对话框和快捷键实际执行验收。 |
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/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/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/Resources/zh-Hans.lproj/Localizable.strings b/macos/Resources/zh-Hans.lproj/Localizable.strings
index da08a78d3..8812840d0 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" = "配色主题";
@@ -32,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/Theme/LitheTheme.swift b/macos/Sources/Lithe/Theme/LitheTheme.swift
index 2c66c82ad..3d67bcc32 100644
--- a/macos/Sources/Lithe/Theme/LitheTheme.swift
+++ b/macos/Sources/Lithe/Theme/LitheTheme.swift
@@ -278,6 +278,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)
@@ -287,6 +288,12 @@ enum LitheTheme {
let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
return nsColor(.sidebar, isDark: isDark)
}
+ 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 settingsListSurface: Color { settingsSurface }
static var settingsFont: Font { .custom("Inter-Regular", size: 13) }
static var settingsStrongFont: Font { .custom("Inter-SemiBold", size: 13) }
@@ -298,13 +305,18 @@ enum LitheTheme {
: NSColor(srgbRed: 201.0 / 255, green: 204.0 / 255, blue: 214.0 / 255, alpha: 1)
})
}
- static var settingsPrimaryAction: Color { accent }
static var settingsSelection: Color {
Color(nsColor: NSColor(name: nil) { appearance in
let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
return isDark
- ? NSColor(srgbRed: 46.0 / 255, green: 67.0 / 255, blue: 110.0 / 255, alpha: 1)
- : NSColor(srgbRed: 212.0 / 255, green: 226.0 / 255, blue: 255.0 / 255, alpha: 1)
+ ? 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 {
@@ -315,6 +327,29 @@ enum LitheTheme {
: 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 settingsSelectBackground: Color {
Color(nsColor: NSColor(name: nil) { appearance in
let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
@@ -523,13 +558,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 c474e66ad..1df5e9fc8 100644
--- a/macos/Sources/Lithe/Views/Agent/AgentComposerView.swift
+++ b/macos/Sources/Lithe/Views/Agent/AgentComposerView.swift
@@ -87,7 +87,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)
if showsSubscriptionQuota {
@@ -136,7 +136,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 d7b637f4b..f01e37aa5 100644
--- a/macos/Sources/Lithe/Views/Agent/AgentConversationView.swift
+++ b/macos/Sources/Lithe/Views/Agent/AgentConversationView.swift
@@ -401,7 +401,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 77791f9f5..f4abe6717 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 08dfc0902..c2e275661 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,39 +34,32 @@ struct KeyboardShortcutSettingsView: View {
}
}
}
- .padding(.horizontal, 24)
- .padding(.bottom, 24)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 4)
}
.litheScrollViewChrome(alwaysShowVertical: true, usesCompactScrollers: true)
}
.background(LitheTheme.settingsSurface)
}
- private var header: some View {
- VStack(alignment: .leading, spacing: 14) {
- 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)
- }
- Spacer()
- Button("Restore All Defaults") {
- editingTarget = nil
- validationIssue = nil
- feature.resetAll()
- }
- .buttonStyle(.bordered)
- .lithePointer()
- }
+ 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 {
+ HStack(spacing: 10) {
LitheSettingsSelect(
selection: Binding(
get: { feature.selectedPreset },
set: { preset in
cancelEditing()
+ selectedCommandID = nil
feature.selectPreset(preset)
}
),
@@ -79,62 +68,167 @@ struct KeyboardShortcutSettingsView: View {
accessibilityLabel: "Keymap",
title: { $0.title }
)
-
- LitheSettingsSearchField("Search actions or shortcuts", text: $query) { _ in
- editingTarget = nil
- validationIssue = nil
+ Menu {
+ Button("Restore All Defaults") {
+ cancelEditing()
+ feature.resetAll()
+ }
+ } label: {
+ LitheSystemIcon(systemImage: "gearshape", size: 16)
+ .foregroundStyle(LitheTheme.secondaryText)
}
+ .menuStyle(.borderlessButton)
+ .menuIndicator(.hidden)
+ .tint(LitheTheme.secondaryText)
+ .frame(width: 26)
+ .accessibilityLabel("Restore All Defaults")
+ .lithePointer()
+ Spacer()
}
.foregroundStyle(LitheTheme.primaryText)
- .padding(24)
+ .padding(.horizontal, 16)
+ .padding(.bottom, 12)
}
- 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)
- VStack(spacing: 0) {
- ForEach(Array(section.commands.enumerated()), id: \.element.id) { index, command in
- commandRow(command)
- if index < section.commands.count - 1 {
- Rectangle().fill(LitheTheme.divider).frame(height: 1)
+ private var toolbar: some View {
+ HStack(spacing: 8) {
+ Button {
+ expandedGroups = Set(LitheActionGroup.allCases)
+ } label: {
+ 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 = []
+ cancelEditing()
+ } label: {
+ 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,
+ let command = LitheCommandCatalog.command(id: selectedCommandID) {
+ 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: {
+ LitheIDEAIcon(resourcePath: "expui/general/edit.svg", size: 16, fallbackSystemImage: "pencil", preservesOriginalColors: true)
}
- .background(LitheTheme.sidebar)
- .clipShape(RoundedRectangle(cornerRadius: 6))
- .overlay {
- RoundedRectangle(cornerRadius: 6)
- .stroke(LitheTheme.divider, lineWidth: 1)
+ .menuStyle(.borderlessButton)
+ .tint(LitheTheme.secondaryText)
+ .disabled(selectedCommandID == nil)
+ .help("Edit Shortcuts")
+ .accessibilityLabel("Edit Shortcuts")
+
+ Spacer(minLength: 12)
+ LitheSettingsSearchField("", text: $query) { _ in
+ cancelEditing()
+ selectedCommandID = nil
}
+ .frame(width: 244)
+ .accessibilityLabel("Search actions or shortcuts")
}
+ .font(.system(size: 12))
+ .buttonStyle(.litheNoPress)
+ .foregroundStyle(LitheTheme.secondaryText)
+ .padding(.horizontal, 16)
+ .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 commandSection(_ section: KeyboardShortcutCommandSection) -> some View {
+ 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) {
+ LitheIDEAIcon(resourcePath: "expui/general/chevronRight.svg", size: 16, fallbackSystemImage: "chevron.right", preservesOriginalColors: true)
+ .rotationEffect(.degrees(isExpanded(section.group) ? 90 : 0))
+ LitheIcon(kind: .folder, size: 16)
+ Text(LocalizedStringKey(section.group.rawValue))
+ .font(.system(size: 12.5))
+ Spacer()
+ }
+ .foregroundStyle(LitheTheme.primaryText)
+ .padding(.horizontal, 8)
+ .frame(height: 24)
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.litheNoPress)
+ .disabled(!query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
+
+ if isExpanded(section.group) {
+ ForEach(section.commands) { command in
+ commandRow(command)
+ }
+ }
+ }
+ }
+
+ 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: 0) {
+ Text(LocalizedStringKey(command.title))
+ .font(.system(size: 12.5))
+ .lineLimit(1)
+ Spacer(minLength: 8)
+ }
+ .contentShape(Rectangle())
}
- .frame(minWidth: 160, maxWidth: .infinity, alignment: .leading)
+ .buttonStyle(.litheNoPress)
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))
+ .litheContextMenu {
+ var items: [LitheContextMenuItem] = [
+ .action("Add Shortcut") {
+ beginEditing(commandID: command.id, bindingIndex: nil)
+ }
+ ]
if feature.isCustomized(command.id) {
- Button("Reset") {
+ items.append(.action("Restore Default") {
cancelEditing()
feature.resetCommand(command.id)
- }
- .buttonStyle(.borderless)
- .font(.system(size: 11.5))
- .lithePointer()
+ })
}
+ return items
}
if editingTarget?.commandID == command.id {
@@ -148,6 +242,7 @@ struct KeyboardShortcutSettingsView: View {
onCancel: cancelEditing
)
.id("\(command.id)-\(editingTarget?.bindingIndex ?? -1)")
+ .padding(.leading, 31)
if let validationIssue {
Label {
@@ -155,14 +250,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 {
@@ -170,7 +263,8 @@ 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()
} else {
@@ -178,16 +272,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()
}
}
@@ -196,34 +280,41 @@ struct KeyboardShortcutSettingsView: View {
commandID: String,
index: Int
) -> some View {
- HStack(spacing: 3) {
- Button(binding.displayText) {
- beginEditing(commandID: commandID, bindingIndex: index)
- }
- .buttonStyle(.borderless)
- .font(.system(size: 11.5, weight: .medium, design: .rounded))
- .lithePointer()
-
- Button {
- removeBinding(commandID: commandID, index: index)
- } label: {
- Image(systemName: "xmark")
- .font(.system(size: 8, weight: .bold))
+ 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)
- .help("Remove")
- .lithePointer()
}
- .padding(.horizontal, 7)
- .frame(height: 24)
- .background(LitheTheme.settingsControlBackground)
- .clipShape(RoundedRectangle(cornerRadius: 4))
- .overlay {
- RoundedRectangle(cornerRadius: 4)
- .stroke(LitheTheme.inputBorder, lineWidth: 1)
+ .buttonStyle(.litheNoPress)
+ .lithePointer()
+ .help("Edit Shortcut")
+ .accessibilityLabel(binding.displayText)
+ .litheContextMenu {
+ [LitheContextMenuItem.action("Remove Shortcut") {
+ removeBinding(commandID: commandID, index: index)
+ }]
}
}
+ 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 {
VStack(spacing: 8) {
Image(systemName: "keyboard.badge.ellipsis")
@@ -237,6 +328,7 @@ struct KeyboardShortcutSettingsView: View {
}
private func beginEditing(commandID: String, bindingIndex: Int?) {
+ selectedCommandID = commandID
validationIssue = nil
editingTarget = EditingTarget(commandID: commandID, bindingIndex: bindingIndex)
}
diff --git a/macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift b/macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift
index c5aacec88..5e3f984b4 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)
}
}
@@ -133,8 +129,8 @@ struct ProjectRuntimeSettingsView: View {
.clipShape(RoundedRectangle(cornerRadius: LitheTheme.Metrics.cornerRadius))
.contentShape(Rectangle())
}
- .buttonStyle(LitheTreeRowButtonStyle())
- .foregroundStyle(isSelected ? Color.white : LitheTheme.primaryText)
+ .buttonStyle(.litheNoPress)
+ .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..923e87eee 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…")
@@ -68,11 +68,12 @@ private struct RunConfigurationSettingsContent: View {
.padding(10)
.contentShape(Rectangle())
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.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 13349cde0..62a78437b 100644
--- a/macos/Sources/Lithe/Views/App/SettingsView.swift
+++ b/macos/Sources/Lithe/Views/App/SettingsView.swift
@@ -212,7 +212,7 @@ struct SettingsView: View {
.frame(height: 24)
.contentShape(Rectangle())
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.foregroundStyle(LitheTheme.primaryText)
if expanded {
ForEach(visible) { category in
@@ -243,7 +243,7 @@ struct SettingsView: View {
.background(isSelected ? LitheTheme.settingsSelection : .clear)
.contentShape(Rectangle())
}
- .buttonStyle(LitheTreeRowButtonStyle())
+ .buttonStyle(.litheNoPress)
.foregroundStyle(LitheTheme.primaryText)
}
}
@@ -310,8 +310,51 @@ struct SettingsView: View {
)
}
- @ViewBuilder
private var content: some View {
+ VStack(spacing: 0) {
+ if viewState.selection != .plugins {
+ 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"]
+ case .plugins: ["Plugins"]
+ }
+ }
+
+ @ViewBuilder
+ private var settingsContent: some View {
if filteredCategories.isEmpty {
VStack(spacing: 10) {
Image(systemName: "magnifyingglass")
@@ -343,12 +386,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
@@ -371,8 +409,8 @@ struct SettingsView: View {
case .plugins: EmptyView()
}
}
- .padding(.horizontal, 28)
- .padding(.vertical, 22)
+ .padding(.horizontal, 16)
+ .padding(.bottom, 20)
.frame(maxWidth: .infinity, alignment: .leading)
}
.litheScrollViewChrome(alwaysShowVertical: true, usesCompactScrollers: true)
@@ -386,7 +424,7 @@ struct SettingsView: View {
LitheSettingsSelect(
selection: $settings.colorTheme,
options: AppColorTheme.allCases,
- width: 180,
+ width: 190,
accessibilityLabel: "Color theme",
title: \AppColorTheme.title
)
@@ -430,7 +468,7 @@ struct SettingsView: View {
Button("Remove") {
model.workbenchBackgroundFeature.clear()
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.foregroundStyle(LitheTheme.accent)
.lithePointer()
}
@@ -449,7 +487,7 @@ struct SettingsView: View {
Button("Retry") {
model.workbenchBackgroundFeature.retry()
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.foregroundStyle(LitheTheme.accent)
.lithePointer()
}
@@ -470,7 +508,7 @@ struct SettingsView: View {
LitheSettingsSelect(
selection: $settings.language,
options: AppLanguage.allCases,
- width: 180,
+ width: 190,
accessibilityLabel: "Language",
title: \AppLanguage.title
)
@@ -486,7 +524,7 @@ struct SettingsView: View {
LitheSettingsSelect(
selection: $settings.projectOpenBehavior,
options: ProjectOpenBehavior.allCases,
- width: 180,
+ width: 190,
accessibilityLabel: "Open projects in",
title: \ProjectOpenBehavior.title
)
@@ -582,7 +620,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()
@@ -590,12 +628,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)
- .strokeBorder(LitheTheme.inputBorder, lineWidth: 1)
- }
+ .litheSettingsControlChrome(background: LitheTheme.settingsTextFieldBackground)
HStack(spacing: 6) {
Text("Default directory")
@@ -615,7 +648,7 @@ struct SettingsView: View {
} label: {
Text("Restore Default")
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.foregroundStyle(LitheTheme.accent)
.lithePointer()
}
@@ -1053,13 +1086,15 @@ struct SettingsView: View {
}
group("Pull request description generation") {
- LitheSettingsSelect(
- selection: $settings.commitMessageAI.pullRequestFormat,
- options: PullRequestDescriptionFormat.allCases,
- width: 260,
- accessibilityLabel: "Description format",
- title: \.title
- )
+ row("Description format") {
+ LitheSettingsSelect(
+ selection: $settings.commitMessageAI.pullRequestFormat,
+ options: PullRequestDescriptionFormat.allCases,
+ width: 220,
+ accessibilityLabel: "Description format",
+ title: \PullRequestDescriptionFormat.title
+ )
+ }
if settings.commitMessageAI.pullRequestFormat == .custom {
HStack {
@@ -1133,17 +1168,12 @@ 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)
- .strokeBorder(
- 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)
+ .buttonStyle(.litheNoPress)
.lithePointer()
.popover(isPresented: $viewState.isFormatPickerPresented, arrowEdge: .bottom) {
formatPickerPopover
@@ -1252,7 +1282,7 @@ struct SettingsView: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.litheRowHover(
isActive: isSelected,
cornerRadius: 5,
@@ -1278,12 +1308,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)
- .strokeBorder(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)
@@ -1463,26 +1488,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/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 47216eac3..0a411a3ca 100644
--- a/macos/Sources/Lithe/Views/App/WelcomeView.swift
+++ b/macos/Sources/Lithe/Views/App/WelcomeView.swift
@@ -82,7 +82,8 @@ struct WelcomeView: View {
.frame(width: 28, height: 28)
.litheRowHover(isActive: false, cornerRadius: 6, activeBackground: selectionColor)
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
+ .lithePointer()
.foregroundStyle(mutedColor)
.help("Settings")
.accessibilityLabel("Settings")
@@ -118,19 +119,19 @@ struct WelcomeView: View {
Button("New Project") {
model.chooseProject(title: "New Project", prompt: "Choose Folder")
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.welcomeActionStyle(foreground: textColor, border: borderColor, hover: hoverColor)
Button("Open") {
model.chooseProject()
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.welcomeActionStyle(foreground: textColor, border: borderColor, hover: hoverColor)
Button("Clone Repository") {
model.showCloneRepository()
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.welcomeActionStyle(foreground: textColor, border: borderColor, hover: hoverColor)
}
.padding(.horizontal, 14)
@@ -201,7 +202,8 @@ struct WelcomeView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
+ .lithePointer()
.disabled(!exists)
Spacer(minLength: 0)
diff --git a/macos/Sources/Lithe/Views/Components/LitheContextMenu.swift b/macos/Sources/Lithe/Views/Components/LitheContextMenu.swift
index 911210f61..4d6b04a54 100644
--- a/macos/Sources/Lithe/Views/Components/LitheContextMenu.swift
+++ b/macos/Sources/Lithe/Views/Components/LitheContextMenu.swift
@@ -304,7 +304,7 @@ private struct LitheContextMenuRow: View {
}
.padding(.horizontal, settingsStyle ? 6 : 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 b4def1276..d45b08067 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 fontSize: CGFloat = 13
- static let checkmarkWidth: CGFloat = 14
- static let itemSpacing: CGFloat = 8
+ static let controlHeight: CGFloat = 28
+ static let controlCornerRadius: CGFloat = 4
+ static let fontSize: CGFloat = 12.5
+ 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 = 10 * itemHeight + 2 * popupPadding
+}
+
+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 {
@@ -31,14 +52,7 @@ struct LitheSettingsSearchField: View {
var body: some View {
HStack(spacing: 7) {
- HStack(spacing: 1) {
- Image(systemName: "magnifyingglass")
- .font(.system(size: 12, weight: .medium))
- Image(systemName: "chevron.down")
- .font(.system(size: 6, weight: .medium))
- }
- .foregroundStyle(LitheTheme.tertiaryText)
- .accessibilityHidden(true)
+ LitheIDEAIcon(resourcePath: "expui/general/search.svg", size: 16, fallbackSystemImage: "magnifyingglass", preservesOriginalColors: true)
TextField(placeholder, text: $text)
.textFieldStyle(.plain)
@@ -53,18 +67,17 @@ struct LitheSettingsSearchField: View {
.font(.system(size: 11))
.foregroundStyle(LitheTheme.tertiaryText)
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.lithePointer()
.help("Clear search")
}
}
.padding(.horizontal, 9)
.frame(height: 28)
- .litheRoundedControlBackground(LitheTheme.settingsSurface)
- .overlay {
- RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)
- .strokeBorder((externalFocus?.wrappedValue ?? isFocused) ? LitheTheme.inputFocusBorder : LitheTheme.settingsSearchBorder, lineWidth: 1)
- }
+ .litheSettingsControlChrome(
+ background: .clear,
+ border: (externalFocus?.wrappedValue ?? isFocused) ? LitheTheme.settingsControlAccent : LitheTheme.settingsControlBorder
+ )
.onChange(of: text) { value in
onTextChanged?(value)
}
@@ -82,7 +95,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,
@@ -106,14 +120,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)))
@@ -129,68 +140,74 @@ 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.settingsSelectBackground)
+ .frame(width: width, height: SettingsSelectMetrics.controlHeight, alignment: .leading)
+ .litheSettingsControlChrome(
+ background: LitheTheme.settingsSelectBackground,
+ border: isPresented ? LitheTheme.settingsControlAccent : LitheTheme.settingsControlBorder
)
- .overlay {
- RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)
- .strokeBorder(isPresented ? LitheTheme.inputFocusBorder : LitheTheme.settingsSearchBorder, lineWidth: 1)
- }
+ .background(LitheSettingsSelectAnchorView(reference: popupAnchor))
.contentShape(Rectangle())
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.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(LitheTheme.settingsFont)
- .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")
- }
+ .accessibilityValue(Text(LocalizedStringKey(title(selection))))
+ .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,
+ 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)
+ )
+ let popup = content.environment(\.locale, locale)
+ LitheSettingsSelectPopupPresenter.shared.show(
+ ownerID: popupID,
+ content: AnyView(popup),
+ state: state,
+ anchorWindow: window,
+ 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(name: "Inter-Regular", size: SettingsSelectMetrics.fontSize)
?? NSFont.systemFont(ofSize: SettingsSelectMetrics.fontSize)
@@ -198,14 +215,279 @@ 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 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 {
+ let selectedIndex: Int
+ @Published var highlightedIndex: Int
+ @Published var keyboardScrollIndex: Int?
+ @Published var popupHeight: CGFloat = 0
+ let optionCount: Int
+ let onChoose: (Int) -> Void
+
+ init(selectedIndex: Int, optionCount: Int, onChoose: @escaping (Int) -> Void) {
+ self.selectedIndex = selectedIndex
+ 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
+ keyboardScrollIndex = highlightedIndex
+ 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 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) }
+ .onChange(of: state.keyboardScrollIndex) { index in
+ if let index { proxy.scrollTo(index) }
+ }
+ }
+ .scrollContentBackground(.hidden)
+ .frame(width: width, height: state.popupHeight)
+ .litheSettingsControlChrome(
+ background: LitheTheme.settingsPopupBackground,
+ border: LitheTheme.settingsPopupBorder,
+ cornerRadius: SettingsSelectMetrics.popupCornerRadius
+ )
+ .clipShape(RoundedRectangle(cornerRadius: SettingsSelectMetrics.popupCornerRadius))
+ }
+
+ var rows: some View {
+ VStack(spacing: 0) {
+ ForEach(Array(options.enumerated()), id: \.offset) { index, option in
+ Button {
+ state.onChoose(index)
+ } label: {
+ HStack {
+ Text(LocalizedStringKey(title(option)))
+ .font(.system(size: SettingsSelectMetrics.fontSize))
+ .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: 0)
+ }
+ .padding(.horizontal, SettingsSelectMetrics.itemHorizontalPadding)
+ .padding(.vertical, expandsToFitOptions ? 4 : 0)
+ .frame(maxWidth: .infinity, minHeight: SettingsSelectMetrics.itemHeight, alignment: .leading)
+ .litheRowHover(
+ isActive: state.highlightedIndex == index,
+ cornerRadius: SettingsSelectMetrics.controlCornerRadius,
+ activeBackground: LitheTheme.settingsSelection.opacity(isAvailable(option) ? 1 : 0.35)
+ )
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.litheNoPress)
+ .accessibilityAddTraits(state.selectedIndex == index ? .isSelected : [])
+ .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 {
+ @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)
+ 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(
+ 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 weak var anchorWindow: NSWindow?
+ private var anchorFrame: NSRect?
+ private var ownerID: UUID?
+ private var onDismiss: (() -> Void)?
+ private var localEventMonitor: Any?
+ private var globalEventMonitor: Any?
+
+ func show(
+ ownerID: UUID,
+ content: AnyView,
+ state: LitheSettingsSelectPopupState,
+ anchorWindow: NSWindow,
+ anchorFrame: NSRect,
+ size: NSSize,
+ visibleFrame: NSRect,
+ appearance: NSAppearance,
+ onDismiss: @escaping () -> Void
+ ) {
+ dismiss()
+ let frame = LitheSettingsSelectPopupGeometry.frame(
+ anchor: anchorFrame, size: size, visibleFrame: visibleFrame
+ )
+ state.popupHeight = frame.height
+ 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.anchorWindow = anchorWindow
+ self.anchorFrame = anchorFrame
+ 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
+ anchorWindow = nil
+ anchorFrame = nil
+ let closingPanel = panel
+ panel = nil
+ closingPanel?.delegate = nil
+ closingPanel?.orderOut(nil)
+ closingPanel?.close()
+ callback?()
+ }
+
+ func windowDidResignKey(_ notification: Notification) {
+ 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 && !LitheSettingsSelectPopupGeometry.isAnchorClick(
+ event, anchorWindow: self.anchorWindow, anchorFrame: self.anchorFrame
+ ) { 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
}
}
@@ -234,30 +516,24 @@ struct LitheSettingsSegmentedControl: View {
selection = option
} label: {
Text(LocalizedStringKey(title(option)))
- .font(selection == option ? LitheTheme.settingsStrongFont : LitheTheme.settingsFont)
- .foregroundStyle(selection == option ? LitheTheme.primaryText : LitheTheme.secondaryText)
- .frame(maxWidth: .infinity, minHeight: 26)
+ .font(.system(size: 12, weight: .medium))
+ .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
)
}
- .buttonStyle(LitheTreeRowButtonStyle())
+ .buttonStyle(.litheNoPress)
.lithePointer()
+ .accessibilityValue(selection == option ? Text("Selected") : Text("Not selected"))
}
}
.padding(2)
- .frame(width: width, height: 30)
- .background(
- RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)
- .fill(LitheTheme.settingsControlBackground)
- )
- .overlay {
- RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)
- .strokeBorder(LitheTheme.inputBorder, lineWidth: 1)
- }
+ .frame(width: width, height: SettingsSelectMetrics.controlHeight)
+ .litheSettingsControlChrome()
}
}
@@ -284,16 +560,17 @@ struct LitheSettingsCheckbox: View {
} label: {
HStack(spacing: 8) {
ZStack {
- RoundedRectangle(cornerRadius: 4)
- .fill(isOn ? LitheTheme.accent : LitheTheme.inputBackground)
- RoundedRectangle(cornerRadius: 4)
- .strokeBorder(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)
@@ -303,9 +580,11 @@ struct LitheSettingsCheckbox: View {
}
.contentShape(Rectangle())
}
- .buttonStyle(LitheTreeRowButtonStyle())
+ .buttonStyle(.litheNoPress)
.lithePointer()
- .accessibilityLabel(Text(accessibilityLabel))
+ .accessibilityRepresentation {
+ Toggle(accessibilityLabel, isOn: $isOn)
+ }
}
}
@@ -343,8 +622,8 @@ struct LitheSettingsStepper: View where Value: Strideable & Comparable, V
.padding(.horizontal, 8)
Rectangle()
- .fill(LitheTheme.settingsSearchBorder)
- .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))
@@ -354,16 +633,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.settingsControlBackground)
- )
- .overlay {
- RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)
- .strokeBorder(LitheTheme.settingsSearchBorder, 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))
}
@@ -377,11 +649,11 @@ 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)
}
- .buttonStyle(LitheTreeRowButtonStyle())
+ .buttonStyle(.litheNoPress)
.disabled(isDisabled)
.lithePointer()
}
@@ -397,15 +669,11 @@ private struct LitheSettingsTextFieldModifier: ViewModifier {
.font(LitheTheme.settingsFont)
.focused($isFocused)
.padding(.horizontal, 9)
- .frame(height: 30)
- .background(
- RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)
- .fill(LitheTheme.settingsControlBackground)
+ .frame(height: SettingsSelectMetrics.controlHeight)
+ .litheSettingsControlChrome(
+ background: LitheTheme.settingsTextFieldBackground,
+ border: isFocused ? LitheTheme.settingsControlAccent : LitheTheme.settingsControlBorder
)
- .overlay {
- RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)
- .strokeBorder(isFocused ? LitheTheme.inputFocusBorder : LitheTheme.settingsSearchBorder, lineWidth: 1)
- }
.opacity(isEnabled ? 1 : 0.55)
}
}
@@ -421,15 +689,22 @@ private struct LitheSettingsTextEditorModifier: ViewModifier {
.focused($isFocused)
.frame(height: height)
.padding(5)
- .litheRoundedControlBackground(LitheTheme.settingsControlBackground)
- .overlay {
- RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)
- .strokeBorder(isFocused ? LitheTheme.inputFocusBorder : LitheTheme.settingsSearchBorder, lineWidth: 1)
- }
+ .litheSettingsControlChrome(
+ background: LitheTheme.settingsTextFieldBackground,
+ border: isFocused ? LitheTheme.settingsControlAccent : LitheTheme.settingsControlBorder
+ )
}
}
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())
}
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 c710492da..dc3ea99d5 100644
--- a/macos/Sources/Lithe/Views/Git/ChangesSidebarView.swift
+++ b/macos/Sources/Lithe/Views/Git/ChangesSidebarView.swift
@@ -162,7 +162,7 @@ struct ChangesSidebarView: View {
hoverBackground: LitheTheme.hoverBackground
)
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
}
Spacer()
GitPatchToolbar(feature: feature)
@@ -345,7 +345,7 @@ struct ChangesSidebarView: View {
.clipShape(RoundedRectangle(cornerRadius: 4))
.contentShape(Rectangle())
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.lithePointer()
.litheContextMenu {
[
@@ -403,7 +403,7 @@ struct ChangesSidebarView: View {
.clipShape(RoundedRectangle(cornerRadius: 4))
.contentShape(Rectangle())
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.lithePointer()
.litheContextMenu {
[
@@ -462,7 +462,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()
@@ -511,7 +511,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)
@@ -587,7 +587,7 @@ struct ChangesSidebarView: View {
.frame(width: 10, height: 26)
.contentShape(Rectangle())
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.help(LocalizedStringKey(isExpanded ? "Collapse repository" : "Expand repository"))
Button {
@@ -603,7 +603,7 @@ struct ChangesSidebarView: View {
.frame(width: 18, height: 26)
.contentShape(Rectangle())
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.disabled(feature.isCommitting || !repository.changes.contains(where: \.canToggleStaging))
.help(LocalizedStringKey(
allChangesStaged(repository.changes)
@@ -633,7 +633,7 @@ struct ChangesSidebarView: View {
.frame(maxWidth: .infinity, minHeight: 26, alignment: .leading)
.contentShape(Rectangle())
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.help(repository.root.path)
}
.padding(.horizontal, 7)
@@ -681,7 +681,7 @@ struct ChangesSidebarView: View {
.frame(width: 10, height: 24)
.contentShape(Rectangle())
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.help(LocalizedStringKey(expanded.wrappedValue ? "Collapse section" : "Expand section"))
Button {
@@ -693,7 +693,7 @@ struct ChangesSidebarView: View {
.frame(width: 18, height: 24)
.contentShape(Rectangle())
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.disabled(feature.isCommitting || !changes.contains(where: \.canToggleStaging))
.help(LocalizedStringKey(allChangesStaged(changes) ? "Unstage all files" : "Stage all files"))
@@ -712,7 +712,7 @@ struct ChangesSidebarView: View {
.frame(maxWidth: .infinity, minHeight: 24)
.contentShape(Rectangle())
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
}
.padding(.horizontal, 7)
.padding(.leading, leadingInset)
@@ -764,7 +764,7 @@ struct ChangesSidebarView: View {
.frame(width: 28, height: changeRowHeight)
.contentShape(Rectangle())
}
- .buttonStyle(LitheTreeRowButtonStyle())
+ .buttonStyle(.litheNoPress)
.disabled(feature.isCommitting || !change.canToggleStaging)
.help(LocalizedStringKey(change.canToggleStaging
? (isEffectivelyStaged(change) ? "Unstage file" : "Stage file")
@@ -807,7 +807,7 @@ struct ChangesSidebarView: View {
.frame(height: changeRowHeight)
.contentShape(Rectangle())
}
- .buttonStyle(LitheTreeRowButtonStyle())
+ .buttonStyle(.litheNoPress)
}
.padding(.leading, 30 + leadingInset)
.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/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/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 691b91842..c7cb80198 100644
--- a/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift
+++ b/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift
@@ -534,7 +534,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)
@@ -684,7 +684,7 @@ struct WorkbenchView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.contentShape(Rectangle())
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.lithePointer()
.accessibilityIdentifier("project-tab-\(projectModel.id.uuidString)")
@@ -765,7 +765,8 @@ struct WorkbenchView: View {
activeBackground: LitheTheme.subtleSelection
)
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
+ .lithePointer()
.accessibilityIdentifier("project-switcher-\(model.id.uuidString)")
.anchorPreference(
key: ProjectSwitcherButtonBoundsPreferenceKey.self,
@@ -801,7 +802,8 @@ struct WorkbenchView: View {
activeBackground: LitheTheme.subtleSelection
)
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
+ .lithePointer()
.anchorPreference(
key: BranchSwitcherButtonBoundsPreferenceKey.self,
value: .bounds
@@ -1077,7 +1079,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")
@@ -1099,7 +1101,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")
@@ -1123,7 +1125,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")
@@ -1162,7 +1164,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)
@@ -1217,7 +1219,7 @@ struct WorkbenchView: View {
.contentShape(Rectangle())
.litheRowHover(isActive: isSelected, cornerRadius: 6, activeBackground: LitheTheme.subtleSelection)
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.help(configuration.name)
.accessibilityAddTraits(isSelected ? .isSelected : [])
}
@@ -1243,7 +1245,7 @@ struct WorkbenchView: View {
activeBackground: LitheTheme.subtleSelection
)
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.lithePointer()
.foregroundStyle(LitheTheme.secondaryText)
.help("Change workbench background")
@@ -1286,7 +1288,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(
@@ -1374,7 +1376,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
@@ -1492,7 +1494,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)))
@@ -1724,7 +1726,7 @@ struct WorkbenchView: View {
}
.foregroundStyle(isEmphasized ? LitheTheme.primaryText : LitheTheme.secondaryText)
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.lithePointer()
.help(LocalizedStringKey(title))
}
@@ -1868,7 +1870,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
@@ -2461,7 +2463,7 @@ struct WorkbenchBackgroundPicker: View {
Button("Remove") {
model.workbenchBackgroundFeature.clear()
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.foregroundStyle(LitheTheme.accent)
.lithePointer()
}
@@ -2518,7 +2520,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 34b090eb1..c68ff0b5d 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()
}
@@ -117,7 +117,7 @@ struct ProjectSwitcherPopover: View {
isCurrent: isCurrent
)
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.lithePointer()
.litheRowHover(
isActive: isCurrent,
@@ -140,7 +140,7 @@ struct ProjectSwitcherPopover: View {
isCurrent: false
)
}
- .buttonStyle(.plain)
+ .buttonStyle(.litheNoPress)
.disabled(!exists)
.lithePointer()
.litheRowHover(cornerRadius: 5)
diff --git a/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift b/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift
new file mode 100644
index 000000000..e797b1b44
--- /dev/null
+++ b/macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift
@@ -0,0 +1,146 @@
+import AppKit
+import SwiftUI
+import Testing
+@testable import Lithe
+
+@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 {
+ 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)
+ 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))
+ }
+
+ @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 e1b1209d3..cd92ecf38 100644
--- a/shared/platform-feature-matrix.json
+++ b/shared/platform-feature-matrix.json
@@ -1898,8 +1898,9 @@
"macos/Sources/Lithe/Views/Workbench/SplitHandleView.swift",
"macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift",
"macos/Sources/Lithe/Views/Components/LitheSettingsControls.swift",
- "macos/Sources/Lithe/Theme/LitheTheme.swift",
"macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift",
+ "macos/Tests/LitheTests/SettingsSelectPopupGeometryTests.swift",
+ "macos/Sources/Lithe/Theme/LitheTheme.swift",
"macos/Tests/LitheTests/PluginManagementPresentationTests.swift"
],
"implementationStatus": "implemented",
@@ -1913,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 或窗口关闭按钮,确认重开后草稿已清除;亮色模式确认分段选择的选中文字清晰可读;检查搜索框、普通及密码输入框、多行编辑框(含获得与失去焦点的边框)、数值步进器、下拉框和分段选择在各设置子页面中的背景、边框、焦点态及操作行为一致,关闭后重开再检查。"
+ "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",
@@ -1939,7 +1940,7 @@
"verificationStatus": "pending"
},
"owner": "Settings",
- "verification": "macOS 从设置 → 快捷键依次选择 macOS、IntelliJ IDEA Classic、Eclipse,核对运行、调试、查找等对应操作在界面和实际监听器中的绑定;IDEA Classic 的 Stop Run 应为 Control-F2,Eclipse 的 Resume 应为 F8、继承父 keymap 的命令应使用上游绑定,Find、Replace、GotoImplementation 等显式清空的命令应无绑定;分别修改两套预设的同一命令,切换与重启后确认自定义绑定互不覆盖,并验证旧版单套覆盖迁移到 macOS;按命令名称、ID 和按键搜索,制造冲突并恢复当前预设或应用全部默认。Windows 保持现有快捷键编辑、导出和导入验证。",
+ "verification": "macOS 从设置 → 快捷键依次选择 macOS、IntelliJ IDEA Classic、Eclipse,核对运行、调试、查找等操作在界面和实际监听器中的绑定;IDEA Classic 的 Stop Run 应为 Control-F2,Eclipse 的 Resume 应为 F8,继承父 keymap 的命令应使用上游绑定,Find、Replace、GotoImplementation 等显式清空的命令应无绑定;检查分类树初始折叠、全部展开和折叠、分类展开、搜索时自动显示匹配操作、选中行高亮、右侧紧凑键帽及工具栏编辑菜单;通过键帽、工具栏或右键菜单新增、删除与恢复默认,并检查键盘辅助功能;分别修改两套预设的同一命令,切换与重启后确认自定义绑定互不覆盖,并验证旧版单套覆盖迁移到 macOS;按命令名称、ID 和按键搜索,制造冲突并恢复当前预设或应用全部默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。Windows 保持现有快捷键编辑、导出和导入验证。",
"notes": "Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。"
},
{