From d19461e96201b81875815c87248b80faeb88cc9b Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 14:35:10 +0800 Subject: [PATCH 1/2] =?UTF-8?q?style(macos):=20=E5=AF=B9=E9=BD=90=E6=AC=A2?= =?UTF-8?q?=E8=BF=8E=E9=A1=B5=E4=B8=8E=20IDEA=20=E7=A4=BE=E5=8C=BA?= =?UTF-8?q?=E7=89=88=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/development/platform-parity-matrix.csv | 4 +- docs/development/platform-parity-matrix.md | 4 +- macos/Sources/Lithe/Views/App/RootView.swift | 4 +- .../Sources/Lithe/Views/App/WelcomeView.swift | 224 ++++++++++++------ .../Views/Components/LitheContextMenu.swift | 93 +++++--- shared/platform-feature-matrix.json | 4 +- 6 files changed, 217 insertions(+), 116 deletions(-) diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv index 1c6c29e25..631ef4792 100644 --- a/docs/development/platform-parity-matrix.csv +++ b/docs/development/platform-parity-matrix.csv @@ -72,7 +72,7 @@ diagnostics-export,可观测性,诊断与日志,诊断包导出与脱敏,已实 application-settings,设置,应用配置,应用、项目与运行设置,已实现,待验证,已实现,待验证,Settings,修改应用级和项目级设置,验证持久化、迁移、重启恢复和无效值提示;macOS 保持设置窗口打开切换亮色、暗色和跟随系统模式,并改变系统外观,确认标题栏与面板背景同色、窗口按钮可用且可拖动;设置窗口应使用普通窗口层级,切换到其他应用时不置顶遮挡,关闭后重开再检查。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Tests/LitheTests/SettingsAppearanceContainerTests.swift,windows/tauri/src/features/settings keymap-customization,设置,应用配置,快捷键查看与自定义,已实现,待验证,已实现,待验证,Settings,从设置 → 快捷键打开编辑器,按命令名称、ID 和按键搜索,修改跳转到实现的快捷键、制造冲突并恢复默认;导出再导入 keybindings.json,确认预设与自定义绑定恢复且命令实际执行。,Windows 已接入现有快捷键编辑器,修复 #718 的占位入口;Linux 前端回归验证不替代 Windows WebView2、原生文件对话框和快捷键实际执行验收。,macos/Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift; macos/Sources/Lithe/Models/Keymap,windows/tauri/src/features/keymaps; windows/tauri/src/features/settings/components/macos-settings-panels.tsx; windows/tauri/src/features/settings/components/tabs/keyboard-settings.tsx; windows/tauri/src/features/settings/components/keyboard-settings-routing.test.tsx plugin-extension-management,扩展,插件与主题,插件、语言服务与主题管理,已实现,待验证,已实现,待验证,Extensions,启用、禁用和恢复插件/主题,确认启动加载、失败隔离和设置持久化。,,macos/Sources/Lithe/Views/App/PluginManagementView.swift; macos/Sources/Lithe/Platform/MacOS/Plugins,windows/tauri/src/extensions; windows/tauri/src/features/settings -onboarding-welcome,启动与窗口,首次启动,欢迎页、项目入口与首次启动引导,已实现,待验证,已实现,待验证,Onboarding,清空首次启动状态,验证打开项目、克隆项目、最近项目和跳过引导。,,macos/Sources/Lithe/Views/App/WelcomeView.swift; macos/Sources/Lithe/Views/Workspace,windows/tauri/src/features/onboarding; windows/tauri/src/features/bootstrap +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 workbench-panes,工作台,命令与布局,分栏、面板与工具窗布局,已实现,待验证,已实现,待验证,Workbench,打开、关闭、移动和调整面板,验证布局持久化及高频拖动稳定性;在 macOS 启用背景图后检查项目栏、编辑器、底部及右侧工具窗之间是否仍有清晰的分割区间;将 macOS 左侧工具窗缩至 30pt 并确认目录树始终贴左、不随宽度向右移动、内容不溢出、右上和右下圆角仍可见,关闭并重新打开项目后仍保持窄侧栏及其他布局尺寸,重新展开后布局正常;在 Maven 项目产生构建输出时确认左侧模块按钮可滚动、设置按钮始终完整显示;在亮色主题且有未读通知时确认未打开的铃铛图标清晰可见、打开通知面板后白色图标与蓝色背景成对出现;在深色 Lithe 主题下对照外框渐变、面板对比度、背景图片回退、左右工具窗的 40pt 按钮槽、30pt 选中块及 IDEA Community 图标对比度,悬停工具窗按钮时确认箭头光标,检查项目栏标题颜色和弹窗主按钮主题色。,,macos/Sources/Lithe/Views/Workbench; macos/Sources/Lithe/Views/Workspace/ProjectSidebarView.swift; macos/Sources/Lithe/Views/Components/LitheSplitPaneView.swift; macos/Sources/Lithe/Services/Workbench/WorkbenchLayoutStore.swift; macos/Sources/Lithe/Views/App/RootView.swift; macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Theme/LitheTheme.swift; macos/Resources/IDEAIcons/expui; macos/Resources/IDEAIcons/terminal,windows/tauri/src/features/panes; windows/tauri/src/features/layout workbench-activity-tooltips,工作台,工作台布局,活动栏图标的稳定悬停说明,已实现,待验证,已实现,待验证,Workbench,逐一悬停左上导航、左下工具入口及右侧通知/插件/Maven,快速切换图标并移出活动栏;确认始终只显示当前图标说明,面板更新和滚动后不残留旧提示,禁用入口可显示说明,短文字自适应且提示不超出窗口边缘。,,macos/Sources/Lithe/Views/Workbench/WorkbenchHoverTooltip.swift; macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift; macos/Tests/LitheTests/WorkbenchHoverTooltipTests.swift,windows/tauri/src/features/layout/components/sidebar/sidebar-pane-selector.tsx; windows/tauri/src/features/layout/components/footer/footer-tab-control.tsx; windows/tauri/src/features/layout/components/plugin-activity-rail.tsx @@ -83,7 +83,7 @@ remote-workspace,工作区,远程开发,远程连接与远程路径工作区,未 vim-mode,编辑器,编辑模式,Vim 模式与命令状态,未实现,不适用,已实现,待验证,Editor,Windows 验证 Normal/Insert/Visual 模式、命令执行和设置持久化;macOS 需要确认产品范围。,,macos/Sources/Lithe/Views/Editor,windows/tauri/src/features/vim; windows/tauri/src/features/editor workbench-notifications,可观测性,通知与资源,操作通知、错误提示与自动消失,已实现,待验证,已实现,待验证,Workbench,触发成功、警告和失败通知,验证队列、关闭、超时和重复通知。,,macos/Sources/Lithe/Application/Features/WorkbenchNotificationFeatureModel.swift,windows/tauri/src/features/notifications memory-monitor,可观测性,通知与资源,应用与语言服务内存监控,已实现,待验证,已实现,待验证,Workbench,打开内存指标并观察应用、语言服务和总内存变化,确认采样失败不会阻塞工作台。,,macos/Sources/Lithe/Services/Monitoring/MemoryUsageMonitor.swift,windows/tauri/src/features/layout/services/memory-api.ts; windows/tauri/src/platform/tauri-core.ts -updates-check-download,发行与更新,应用更新,检查、下载与安装更新,已实现,待验证,已实现,待验证,Release,使用 preview 更新清单验证检查、下载、校验、安装和重启;从菜单或欢迎页手动检查到新版本时直接弹出更新详情并可一键安装,设置页检查到新版本时在检查按钮旁显示安装按钮,启动时的自动检查只在标题栏提示。,,macos/Sources/Lithe/Views/App/UpdateControl.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Platform/MacOS/Updates,windows/tauri/src/features/layout/components/app-update-control.tsx; windows/tauri/src/features/layout/components/app-update-details-dialog.tsx; windows/tauri/src/features/settings/components/macos-settings-panels.tsx; windows/tauri/src/features/settings/hooks/use-updater.ts +updates-check-download,发行与更新,应用更新,检查、下载与安装更新,已实现,待验证,已实现,待验证,Release,使用 preview 更新清单验证检查、下载、校验、安装和重启;从应用菜单或欢迎页左下角齿轮菜单手动检查到新版本时直接弹出更新详情并可一键安装,设置页检查到新版本时在检查按钮旁显示安装按钮,启动时的自动检查只在标题栏提示。,,macos/Sources/Lithe/Views/App/UpdateControl.swift; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Platform/MacOS/Updates,windows/tauri/src/features/layout/components/app-update-control.tsx; windows/tauri/src/features/layout/components/app-update-details-dialog.tsx; windows/tauri/src/features/settings/components/macos-settings-panels.tsx; windows/tauri/src/features/settings/hooks/use-updater.ts updates-failure-rollback,发行与更新,应用更新,失败重试与回滚,部分实现,待验证,已实现,待验证,Release,模拟清单错误、下载失败和安装失败,确认重试、回滚和用户提示。,macOS 回退更新方案仍在 #734 跟进。,macos/Sources/Lithe/Platform/MacOS/Updates,docs/releases/windows-updater.md; windows/tauri/src/features/settings/hooks/use-updater.ts community-feed,社区,LINUX DO 社区,主题列表与详情,未实现,不适用,未实现,不适用,Community,如果重新引入社区入口,在 macOS 和 Windows 验证 Latest/Top、主题详情、分页和浏览器跳转。,macOS 的 LINUX DO 插件已退役,当前两端均无社区入口。,macos/Sources/Lithe/Platform/MacOS/Plugins/MacPluginPackageStore.swift,windows/tauri/src/features community-auth,社区,LINUX DO 社区,社区认证与请求失败处理,未实现,不适用,未实现,不适用,Community,如果重新引入社区入口,在 macOS 和 Windows 验证认证、过期、限流和网络错误。,macOS 的 LINUX DO 插件已退役,现存 fixture 不代表产品接入。,macos/Sources/Lithe/Platform/MacOS/Plugins/MacPluginPackageStore.swift; shared/fixtures/community,windows/tauri/src/features diff --git a/docs/development/platform-parity-matrix.md b/docs/development/platform-parity-matrix.md index e55e4495b..125e25bac 100644 --- a/docs/development/platform-parity-matrix.md +++ b/docs/development/platform-parity-matrix.md @@ -223,7 +223,7 @@ | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | -| 首次启动 | **欢迎页、项目入口与首次启动引导**
onboarding-welcome | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/WelcomeView.swift`、`macos/Sources/Lithe/Views/Workspace` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/onboarding`、`windows/tauri/src/features/bootstrap` | Onboarding | 清空首次启动状态,验证打开项目、克隆项目、最近项目和跳过引导。 | | +| 首次启动 | **欢迎页、项目入口与首次启动引导**
onboarding-welcome | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/WelcomeView.swift`、`macos/Sources/Lithe/Views/Workspace` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/onboarding`、`windows/tauri/src/features/bootstrap` | Onboarding | 清空首次启动状态,验证打开项目、克隆项目、最近项目和跳过引导;在 macOS 深色与浅色模式检查欢迎页布局、颜色和交互,确认默认及最小内容尺寸均为 800×650、可向屏幕可用区域放大,左下角齿轮菜单采用设置页下拉框配色并可进入设置与检查更新。 | | @@ -232,7 +232,7 @@ | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | -| 应用更新 | **检查、下载与安装更新**
updates-check-download | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/UpdateControl.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Platform/MacOS/Updates` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/layout/components/app-update-control.tsx`、`windows/tauri/src/features/layout/components/app-update-details-dialog.tsx`、`windows/tauri/src/features/settings/components/macos-settings-panels.tsx`、`windows/tauri/src/features/settings/hooks/use-updater.ts` | Release | 使用 preview 更新清单验证检查、下载、校验、安装和重启;从菜单或欢迎页手动检查到新版本时直接弹出更新详情并可一键安装,设置页检查到新版本时在检查按钮旁显示安装按钮,启动时的自动检查只在标题栏提示。 | | +| 应用更新 | **检查、下载与安装更新**
updates-check-download | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/UpdateControl.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Platform/MacOS/Updates` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/layout/components/app-update-control.tsx`、`windows/tauri/src/features/layout/components/app-update-details-dialog.tsx`、`windows/tauri/src/features/settings/components/macos-settings-panels.tsx`、`windows/tauri/src/features/settings/hooks/use-updater.ts` | Release | 使用 preview 更新清单验证检查、下载、校验、安装和重启;从应用菜单或欢迎页左下角齿轮菜单手动检查到新版本时直接弹出更新详情并可一键安装,设置页检查到新版本时在检查按钮旁显示安装按钮,启动时的自动检查只在标题栏提示。 | | | 应用更新 | **失败重试与回滚**
updates-failure-rollback | 🟡 部分实现
🔍 待验证
`macos/Sources/Lithe/Platform/MacOS/Updates` | ✅ 已实现
🔍 待验证
`docs/releases/windows-updater.md`、`windows/tauri/src/features/settings/hooks/use-updater.ts` | Release | 模拟清单错误、下载失败和安装失败,确认重试、回滚和用户提示。 | macOS 回退更新方案仍在 #734 跟进。 | diff --git a/macos/Sources/Lithe/Views/App/RootView.swift b/macos/Sources/Lithe/Views/App/RootView.swift index 51fe67798..0ebfef0e3 100644 --- a/macos/Sources/Lithe/Views/App/RootView.swift +++ b/macos/Sources/Lithe/Views/App/RootView.swift @@ -336,7 +336,7 @@ enum LitheWindowLayout: Equatable { case workspace case standalone - static let welcomeContentSize = NSSize(width: 900, height: 620) + static let welcomeContentSize = NSSize(width: 800, height: 650) static let workspaceContentSize = NSSize(width: 1440, height: 900) static let standaloneContentSize = NSSize(width: 1200, height: 760) static let standaloneMinimumContentSize = NSSize(width: 760, height: 480) @@ -353,7 +353,7 @@ enum LitheWindowLayout: Equatable { var minimumContentSize: NSSize { switch self { - case .welcome: NSSize(width: 820, height: 560) + case .welcome: Self.welcomeContentSize case .workspace: NSSize(width: 980, height: 640) case .standalone: Self.standaloneMinimumContentSize } diff --git a/macos/Sources/Lithe/Views/App/WelcomeView.swift b/macos/Sources/Lithe/Views/App/WelcomeView.swift index 6a0473ebd..dbb9748f4 100644 --- a/macos/Sources/Lithe/Views/App/WelcomeView.swift +++ b/macos/Sources/Lithe/Views/App/WelcomeView.swift @@ -2,6 +2,8 @@ import AppKit import SwiftUI struct WelcomeView: View { + @Environment(\.colorScheme) private var colorScheme + @Environment(\.locale) private var locale @EnvironmentObject private var model: AppModel @EnvironmentObject private var updateChecker: UpdateChecker @State private var projectFilter = "" @@ -9,118 +11,123 @@ struct WelcomeView: View { @State private var hoveredProjectMenuID: String? @FocusState private var searchFocused: Bool + // JetBrains/intellij-community: platform/platform-resources/src/themes/islands/ManyIslandsDark.theme.json + // and platform/platform-resources/src/themes/expUI/expUI_light.theme.json. + // Keep these welcome colors local so workspace and settings themes remain independent. + private var surface: Color { colorScheme == .dark ? Color(red: 0.098, green: 0.102, blue: 0.110) : .white } + private var sidebarSurface: Color { colorScheme == .dark ? Color(red: 0.098, green: 0.102, blue: 0.110) : Color(red: 0.969, green: 0.973, blue: 0.980) } + private var textColor: Color { colorScheme == .dark ? Color(red: 0.820, green: 0.827, blue: 0.851) : .black } + private var mutedColor: Color { colorScheme == .dark ? Color(red: 0.451, green: 0.463, blue: 0.486) : Color(red: 0.424, green: 0.439, blue: 0.494) } + private var separatorColor: Color { colorScheme == .dark ? Color(red: 0.149, green: 0.157, blue: 0.173) : Color(red: 0.875, green: 0.882, blue: 0.898) } + private var selectionColor: Color { colorScheme == .dark ? Color(red: 0.165, green: 0.263, blue: 0.443) : Color(red: 0.831, green: 0.886, blue: 1) } + private var hoverColor: Color { colorScheme == .dark ? .white.opacity(0.063) : Color(red: 0.922, green: 0.925, blue: 0.941) } + private var borderColor: Color { colorScheme == .dark ? Color(red: 0.251, green: 0.263, blue: 0.290) : Color(red: 0.788, green: 0.800, blue: 0.839) } + var body: some View { HStack(spacing: 0) { welcomeSidebar - Rectangle().fill(LitheTheme.divider.opacity(0.55)).frame(width: 1) + Rectangle().fill(separatorColor).frame(width: 1) projectsContent } - .background(LitheTheme.editor) + .background(surface.ignoresSafeArea(.container, edges: .top)) .background(WelcomeInitialFocusReset()) } private var welcomeSidebar: some View { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 11) { - LitheIcons.appLogo(size: 42) - VStack(alignment: .leading, spacing: 3) { + LitheIcons.appLogo(size: 28) + VStack(alignment: .leading, spacing: 0) { Text("Lithe") - .font(.system(size: 18, weight: .semibold)) - .foregroundStyle(LitheTheme.primaryText) - Text("\(updateChecker.versionDescription) · macOS") - .font(LitheTheme.smallFont) - .foregroundStyle(LitheTheme.secondaryText) - updatePrompt + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(textColor) + Text(updateChecker.versionDescription) + .font(.system(size: 11)) + .foregroundStyle(mutedColor) } } .padding(.horizontal, 20) - .padding(.top, 28) - .padding(.bottom, 30) + .padding(.top, 26) + .padding(.bottom, 38) HStack(spacing: 9) { LitheIcon(kind: .folder, size: 15) Text("Projects") .font(.system(size: 13, weight: .medium)) } - .foregroundStyle(LitheTheme.primaryText) + .foregroundStyle(textColor) .padding(.horizontal, 14) .frame(maxWidth: .infinity, alignment: .leading) - .frame(height: 36) - .background(LitheTheme.selection) - .clipShape(RoundedRectangle(cornerRadius: LitheTheme.Metrics.cornerRadius)) + .frame(height: 32) + .background(selectionColor) + .clipShape(RoundedRectangle(cornerRadius: 4)) .padding(.horizontal, 14) Spacer() Button { - model.showSettings() + showSettingsMenu() } label: { - HStack(spacing: 9) { - LitheSystemIcon(systemImage: "gearshape") - .font(.system(size: 14)) - Text("Settings") - .font(.system(size: 12.5)) - Spacer() - } - .foregroundStyle(LitheTheme.secondaryText) - .padding(.horizontal, 14) - .frame(maxWidth: .infinity, alignment: .leading) - .frame(height: 34) - .contentShape(Rectangle()) + LitheIDEAIcon(resourcePath: "expui/general/settings@20x20.svg", size: 14) + .frame(width: 28, height: 28) + .litheRowHover(isActive: false, cornerRadius: 6, activeBackground: selectionColor) } .buttonStyle(.plain) - .lithePointer() - .litheRowHover() + .foregroundStyle(mutedColor) + .help("Settings") + .accessibilityLabel("Settings") .padding(.horizontal, 14) .padding(.bottom, 14) + .frame(maxWidth: .infinity, alignment: .leading) } - .frame(width: 240) + .frame(width: 224) .background( - LitheTheme.sidebar + sidebarSurface .ignoresSafeArea(.container, edges: .top) ) } - @ViewBuilder - private var updatePrompt: some View { - VStack(alignment: .leading, spacing: 8) { - UpdateControl() - StableRollbackControl(compact: true) - } - } - private var projectsContent: some View { VStack(spacing: 0) { HStack(spacing: 10) { HStack(spacing: 8) { LitheSystemIcon(systemImage: "magnifyingglass") .font(.system(size: 12.5)) - .foregroundStyle(LitheTheme.secondaryText) + .foregroundStyle(mutedColor) TextField("Search projects", text: $projectFilter) .textFieldStyle(.plain) .font(.system(size: 13)) .focused($searchFocused) } - .litheSearchField(isFocused: searchFocused, height: 30) - .frame(maxWidth: 300) + .foregroundStyle(textColor) + .padding(.horizontal, 8) + .frame(maxWidth: 220) Spacer() - Button("Clone") { - model.showCloneRepository() + Button("New Project") { + model.chooseProject(title: "New Project", prompt: "Choose Folder") } - .buttonStyle(LitheSecondaryButtonStyle()) + .buttonStyle(.plain) + .welcomeActionStyle(foreground: textColor, border: borderColor, hover: hoverColor) Button("Open") { model.chooseProject() } - .buttonStyle(LithePrimaryButtonStyle()) + .buttonStyle(.plain) + .welcomeActionStyle(foreground: textColor, border: borderColor, hover: hoverColor) + + Button("Clone Repository") { + model.showCloneRepository() + } + .buttonStyle(.plain) + .welcomeActionStyle(foreground: textColor, border: borderColor, hover: hoverColor) } - .padding(.horizontal, 20) - .frame(height: 66) + .padding(.horizontal, 14) + .frame(height: 68) - Rectangle().fill(LitheTheme.divider).frame(height: 1) - .padding(.horizontal, 18) + Rectangle().fill(separatorColor).frame(height: 1) + .padding(.horizontal, 12) if filteredProjects.isEmpty { emptyProjectsState @@ -132,25 +139,25 @@ struct WelcomeView: View { } } .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 14) - .padding(.vertical, 10) + .padding(.horizontal, 24) + .padding(.vertical, 14) } } } - .background(LitheTheme.editor) + .background(surface.ignoresSafeArea(.container, edges: .top)) } private var emptyProjectsState: some View { VStack(spacing: 10) { Image(systemName: "folder.badge.plus") .font(.system(size: 32, weight: .light)) - .foregroundStyle(LitheTheme.secondaryText) + .foregroundStyle(mutedColor) Text(model.recentProjects.isEmpty ? "No recent projects" : "No matching projects") .font(.system(size: 14, weight: .medium)) - .foregroundStyle(LitheTheme.primaryText) + .foregroundStyle(textColor) Text("Open a local folder to start working.") .font(LitheTheme.uiFont) - .foregroundStyle(LitheTheme.secondaryText) + .foregroundStyle(mutedColor) } .frame(maxWidth: .infinity, maxHeight: .infinity) } @@ -161,21 +168,21 @@ struct WelcomeView: View { Button { if exists { model.openProject(project.url) } } label: { - HStack(spacing: 12) { + HStack(spacing: 8) { Text(initials(for: project.name)) - .font(.system(size: 12, weight: .bold, design: .rounded)) + .font(.system(size: 10, weight: .bold, design: .rounded)) .foregroundStyle(.white) - .frame(width: 34, height: 34) - .background(exists ? color(for: project.name) : LitheTheme.raised) - .clipShape(RoundedRectangle(cornerRadius: 8)) + .frame(width: 20, height: 20) + .background(exists ? color(for: project.name) : hoverColor) + .clipShape(RoundedRectangle(cornerRadius: 4)) VStack(alignment: .leading, spacing: 3) { Text(project.name) .font(.system(size: 13.5, weight: .medium)) - .foregroundStyle(exists ? LitheTheme.primaryText : LitheTheme.secondaryText) + .foregroundStyle(exists ? textColor : mutedColor) Text(project.path.replacingOccurrences(of: NSHomeDirectory(), with: "~")) .font(.system(size: 11.5)) - .foregroundStyle(LitheTheme.secondaryText) + .foregroundStyle(mutedColor) .lineLimit(1) .truncationMode(.middle) } @@ -185,7 +192,6 @@ struct WelcomeView: View { .contentShape(Rectangle()) } .buttonStyle(.plain) - .lithePointer() .disabled(!exists) Spacer(minLength: 0) @@ -202,11 +208,11 @@ struct WelcomeView: View { } } label: { ZStack { - RoundedRectangle(cornerRadius: LitheTheme.Metrics.cornerRadius) - .fill(hoveredProjectMenuID == project.id ? LitheTheme.hoverBackground : .clear) + RoundedRectangle(cornerRadius: 4) + .fill(hoveredProjectMenuID == project.id ? hoverColor : .clear) LitheSystemIcon(systemImage: "ellipsis") .font(.system(size: 12, weight: .semibold)) - .foregroundStyle(LitheTheme.secondaryText) + .foregroundStyle(mutedColor) } .frame(width: 28, height: 28) .contentShape(Rectangle()) @@ -216,16 +222,15 @@ struct WelcomeView: View { } .menuStyle(.borderlessButton) .menuIndicator(.hidden) - .lithePointer() .frame(width: 28, height: 28) .opacity(hoveredProjectID == project.id ? 1 : 0) .allowsHitTesting(hoveredProjectID == project.id) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 12) - .frame(height: 52) - .background(hoveredProjectID == project.id ? LitheTheme.hoverBackground : .clear) - .clipShape(RoundedRectangle(cornerRadius: LitheTheme.Metrics.cornerRadius)) + .frame(height: 68) + .background(hoveredProjectID == project.id ? selectionColor : .clear) + .clipShape(RoundedRectangle(cornerRadius: 4)) .onHover { isHovering in hoveredProjectID = isHovering ? project.id : nil } @@ -252,6 +257,36 @@ struct WelcomeView: View { } } + private func showSettingsMenu() { + guard let window = NSApp.keyWindow else { return } + let screenPoint = NSApp.currentEvent.flatMap { event in + event.window === window ? window.convertPoint(toScreen: event.locationInWindow) : nil + } ?? NSPoint(x: window.frame.minX + 28, y: window.frame.minY + 28) + var items: [LitheContextMenuItem] = [ + .action("Settings…", action: { model.showSettings() }), + .separator + ] + if case .available(let version, _) = updateChecker.status { + items.append(.action("Update to \(version)…", action: { updateChecker.presentDetails() })) + } else { + items.append(.action( + "Check for Updates…", + isEnabled: !updateChecker.isBusy, + action: { + Task { await updateChecker.checkForUpdates(manual: true, presentingDetails: true) } + } + )) + } + LitheContextMenuPresenter.shared.show( + items: items, + at: screenPoint, + appearance: window.effectiveAppearance, + locale: locale, + opensUpward: true, + settingsStyle: true + ) + } + private func initials(for name: String) -> String { let words = name.split(whereSeparator: { !$0.isLetter && !$0.isNumber }) let characters = words.prefix(2).compactMap(\.first) @@ -260,11 +295,15 @@ struct WelcomeView: View { private func color(for value: String) -> Color { let palette: [Color] = [ - Color(red: 0.90, green: 0.43, blue: 0.28), - Color(red: 0.12, green: 0.63, blue: 0.68), - Color(red: 0.28, green: 0.53, blue: 0.88), - Color(red: 0.30, green: 0.66, blue: 0.48), - Color(red: 0.70, green: 0.52, blue: 0.12) + Color(red: 0.878, green: 0.533, blue: 0.333), + Color(red: 0.690, green: 0.545, blue: 0.078), + Color(red: 0.631, green: 0.639, blue: 0.349), + Color(red: 0.231, green: 0.573, blue: 0.722), + Color(red: 0.208, green: 0.455, blue: 0.941), + Color(red: 0.584, green: 0.353, blue: 0.878), + Color(red: 0.373, green: 0.678, blue: 0.396), + Color(red: 0.141, green: 0.639, blue: 0.580), + Color(red: 0.784, green: 0.302, blue: 0.561) ] var hash: UInt64 = 1_469_598_103_934_665_603 for byte in value.utf8 { @@ -275,6 +314,35 @@ struct WelcomeView: View { } } +private struct WelcomeActionStyle: ViewModifier { + let foreground: Color + let border: Color + let hover: Color + @State private var isHovering = false + + func body(content: Content) -> some View { + content + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(foreground) + .padding(.horizontal, 13) + .frame(height: 28) + .background(isHovering ? hover : .clear) + .clipShape(RoundedRectangle(cornerRadius: 5)) + .overlay { + RoundedRectangle(cornerRadius: 5) + .strokeBorder(border, lineWidth: 1) + } + .contentShape(Rectangle()) + .onHover { isHovering = $0 } + } +} + +private extension View { + func welcomeActionStyle(foreground: Color, border: Color, hover: Color) -> some View { + modifier(WelcomeActionStyle(foreground: foreground, border: border, hover: hover)) + } +} + private struct WelcomeInitialFocusReset: NSViewRepresentable { func makeNSView(context: Context) -> NSView { WelcomeInitialFocusResetView() diff --git a/macos/Sources/Lithe/Views/Components/LitheContextMenu.swift b/macos/Sources/Lithe/Views/Components/LitheContextMenu.swift index 6493d464d..911210f61 100644 --- a/macos/Sources/Lithe/Views/Components/LitheContextMenu.swift +++ b/macos/Sources/Lithe/Views/Components/LitheContextMenu.swift @@ -148,11 +148,13 @@ private final class LitheContextMenuSelection: ObservableObject { } private struct LitheContextMenuContent: View { + @Environment(\.colorScheme) private var colorScheme @ObservedObject var selection: LitheContextMenuSelection let width: CGFloat let submenuWidth: CGFloat let submenuOnLeft: Bool let maximumHeight: CGFloat + let settingsStyle: Bool var body: some View { HStack(alignment: .top, spacing: LitheContextMenuMetrics.submenuSpacing) { @@ -178,6 +180,7 @@ private struct LitheContextMenuContent: View { LitheContextMenuRow( item: item, isSelected: (isChild ? selection.childID : selection.selectedID) == item.id, + settingsStyle: settingsStyle, action: { if case .submenu = item.kind { selection.open(item.id) } else { selection.dismiss(); item.action() } @@ -203,26 +206,42 @@ private struct LitheContextMenuContent: View { if let id { proxy.scrollTo(id) } } } - .frame(width: width, height: min(LitheContextMenuPresenter.menuHeight(for: items), maximumHeight)) - .litheContextMenuSurface() + .frame(width: width, height: min(LitheContextMenuPresenter.menuHeight(for: items, settingsStyle: settingsStyle), maximumHeight)) + .background { + RoundedRectangle(cornerRadius: settingsStyle ? 8 : LitheTheme.Metrics.contextMenuCornerRadius) + .fill(settingsStyle + ? (colorScheme == .dark ? Color(red: 38 / 255, green: 40 / 255, blue: 44 / 255) : .white) + : LitheTheme.contextMenuBackground) + } + .overlay { + RoundedRectangle(cornerRadius: settingsStyle ? 8 : LitheTheme.Metrics.contextMenuCornerRadius) + .stroke(settingsStyle + ? (colorScheme == .dark ? Color(red: 76 / 255, green: 79 / 255, blue: 86 / 255) + : Color(red: 233 / 255, green: 234 / 255, blue: 238 / 255)) + : LitheTheme.panelBorder, lineWidth: 1) + } } } private struct LitheContextMenuRow: View { + @Environment(\.colorScheme) private var colorScheme let item: LitheContextMenuItem let action: (() -> Void)? let onSubmenuHover: ((Bool) -> Void)? let isSelected: Bool + let settingsStyle: Bool private var isHovering: Bool { isSelected } init( item: LitheContextMenuItem, isSelected: Bool, + settingsStyle: Bool, action: @escaping () -> Void, onHover: ((Bool) -> Void)? = nil ) { self.item = item self.isSelected = isSelected + self.settingsStyle = settingsStyle self.action = action self.onSubmenuHover = onHover } @@ -237,24 +256,26 @@ private struct LitheContextMenuRow: View { action?() } label: { HStack(spacing: 9) { - Group { - if let iconKind = item.iconKind { - LitheIcon(kind: iconKind, size: 16) - } else if let systemImage = item.systemImage { - Image(systemName: systemImage) - .font(.system(size: 13, weight: .regular)) - } else { - Color.clear + if !settingsStyle { + Group { + if let iconKind = item.iconKind { + LitheIcon(kind: iconKind, size: 16) + } else if let systemImage = item.systemImage { + Image(systemName: systemImage) + .font(.system(size: 13, weight: .regular)) + } else { + Color.clear + } } + .frame(width: 16, height: 16) + .foregroundStyle(isHovering ? LitheTheme.toolWindowSelectedText : LitheTheme.secondaryText) } - .frame(width: 16, height: 16) - .foregroundStyle( - isHovering ? LitheTheme.toolWindowSelectedText : LitheTheme.secondaryText - ) Text(LocalizedStringKey(item.title)) - .font(Font(LitheContextMenuMetrics.itemFont)) - .foregroundStyle(isHovering ? LitheTheme.toolWindowSelectedText : LitheTheme.primaryText) + .font(settingsStyle ? .system(size: 12.5) : Font(LitheContextMenuMetrics.itemFont)) + .foregroundStyle(isHovering + ? (settingsStyle ? (colorScheme == .dark ? .white : .black) : LitheTheme.toolWindowSelectedText) + : LitheTheme.primaryText) .lineLimit(1) Spacer(minLength: 14) @@ -269,14 +290,19 @@ private struct LitheContextMenuRow: View { .foregroundStyle(isHovering ? LitheTheme.toolWindowSelectedText.opacity(0.78) : LitheTheme.tertiaryText) } } - .padding(.horizontal, 9) - .frame(height: LitheContextMenuMetrics.rowHeight) + .padding(.horizontal, settingsStyle ? 8 : 9) + .frame(height: settingsStyle ? 24 : LitheContextMenuMetrics.rowHeight) .contentShape(Rectangle()) .background { - RoundedRectangle(cornerRadius: 5, style: .continuous) - .fill(isHovering ? LitheTheme.selection : .clear) + RoundedRectangle(cornerRadius: settingsStyle ? 4 : 5, style: .continuous) + .fill(isHovering + ? (settingsStyle + ? (colorScheme == .dark ? Color(red: 42 / 255, green: 67 / 255, blue: 113 / 255) + : Color(red: 208 / 255, green: 223 / 255, blue: 254 / 255)) + : LitheTheme.selection) + : .clear) } - .padding(.horizontal, 5) + .padding(.horizontal, settingsStyle ? 6 : 5) } .buttonStyle(.plain) .disabled(!item.isEnabled) @@ -310,20 +336,23 @@ final class LitheContextMenuPresenter: NSObject, NSWindowDelegate { items: [LitheContextMenuItem], at screenPoint: NSPoint, appearance: NSAppearance?, - locale: Locale + locale: Locale, + opensUpward: Bool = false, + settingsStyle: Bool = false ) { dismiss() guard !items.isEmpty else { return } let menuWidth = Self.menuWidth( for: items, - minimumWidth: LitheContextMenuMetrics.minimumRootWidth + minimumWidth: settingsStyle ? 156 : LitheContextMenuMetrics.minimumRootWidth, + chromeWidth: settingsStyle ? 28 : 67 ) let visibleFrame = NSScreen.screens.first(where: { $0.frame.contains(screenPoint) })?.visibleFrame ?? NSScreen.main?.visibleFrame ?? .zero self.visibleFrame = visibleFrame.insetBy(dx: 6, dy: 6) let maximumHeight = max(1, visibleFrame.height - 12) - let menuHeight = min(Self.menuHeight(for: items), maximumHeight) + let menuHeight = min(Self.menuHeight(for: items, settingsStyle: settingsStyle), maximumHeight) let submenuWidths = items.compactMap { item -> CGFloat? in guard case .submenu(let submenuItems) = item.kind else { return nil } return Self.menuWidth( @@ -337,7 +366,10 @@ final class LitheContextMenuPresenter: NSObject, NSWindowDelegate { } let submenuWidth = submenuWidths.max() ?? 0 let submenuHeight = min(submenuHeights.max() ?? 0, maximumHeight) - let preferredOrigin = NSPoint(x: screenPoint.x - 6, y: screenPoint.y - menuHeight + 6) + let preferredOrigin = NSPoint( + x: screenPoint.x - 6, + y: opensUpward ? screenPoint.y + 6 : screenPoint.y - menuHeight + 6 + ) let origin = NSPoint( x: min(max(preferredOrigin.x, visibleFrame.minX + 6), visibleFrame.maxX - menuWidth - 6), y: min(max(preferredOrigin.y, visibleFrame.minY + 6), visibleFrame.maxY - menuHeight - 6) @@ -354,7 +386,7 @@ final class LitheContextMenuPresenter: NSObject, NSWindowDelegate { } let content = LitheContextMenuContent( selection: selection, width: menuWidth, submenuWidth: submenuWidth, - submenuOnLeft: submenuOnLeft, maximumHeight: maximumHeight + submenuOnLeft: submenuOnLeft, maximumHeight: maximumHeight, settingsStyle: settingsStyle ) .environment(\.locale, locale) @@ -416,7 +448,8 @@ final class LitheContextMenuPresenter: NSObject, NSWindowDelegate { fileprivate static func menuWidth( for items: [LitheContextMenuItem], - minimumWidth: CGFloat + minimumWidth: CGFloat, + chromeWidth: CGFloat = 67 ) -> CGFloat { let widestItem = items.reduce(CGFloat.zero) { width, item in guard case .action = item.kind else { @@ -425,20 +458,20 @@ final class LitheContextMenuPresenter: NSObject, NSWindowDelegate { } return max(width, menuItemWidth(item)) } - let contentWidth = widestItem + 67 + let contentWidth = widestItem + chromeWidth return min( max(contentWidth, minimumWidth), LitheContextMenuMetrics.maximumWidth ) } - fileprivate static func menuHeight(for items: [LitheContextMenuItem]) -> CGFloat { + fileprivate static func menuHeight(for items: [LitheContextMenuItem], settingsStyle: Bool = false) -> CGFloat { items.reduce(LitheContextMenuMetrics.verticalPadding) { height, item in switch item.kind { case .separator: height + LitheContextMenuMetrics.separatorHeight case .action, .submenu: - height + LitheContextMenuMetrics.rowHeight + height + (settingsStyle ? 24 : LitheContextMenuMetrics.rowHeight) } } } diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index f8308ed12..3d3534d27 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -1835,7 +1835,7 @@ "verificationStatus": "pending" }, "owner": "Onboarding", - "verification": "清空首次启动状态,验证打开项目、克隆项目、最近项目和跳过引导。" + "verification": "清空首次启动状态,验证打开项目、克隆项目、最近项目和跳过引导;在 macOS 深色与浅色模式检查欢迎页布局、颜色和交互,确认默认及最小内容尺寸均为 800×650、可向屏幕可用区域放大,左下角齿轮菜单采用设置页下拉框配色并可进入设置与检查更新。" }, { "id": "command-palette", @@ -2105,7 +2105,7 @@ "verificationStatus": "pending" }, "owner": "Release", - "verification": "使用 preview 更新清单验证检查、下载、校验、安装和重启;从菜单或欢迎页手动检查到新版本时直接弹出更新详情并可一键安装,设置页检查到新版本时在检查按钮旁显示安装按钮,启动时的自动检查只在标题栏提示。" + "verification": "使用 preview 更新清单验证检查、下载、校验、安装和重启;从应用菜单或欢迎页左下角齿轮菜单手动检查到新版本时直接弹出更新详情并可一键安装,设置页检查到新版本时在检查按钮旁显示安装按钮,启动时的自动检查只在标题栏提示。" }, { "id": "updates-failure-rollback", From 1a5ca5b13eb7ab6bd649253d0ce7b4360e59998f Mon Sep 17 00:00:00 2001 From: arkleselect Date: Mon, 28 Sep 2026 16:56:03 +0800 Subject: [PATCH 2/2] fix(macOS): restore welcome update and stable rollback actions --- .../Sources/Lithe/Views/App/WelcomeView.swift | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Lithe/Views/App/WelcomeView.swift b/macos/Sources/Lithe/Views/App/WelcomeView.swift index dbb9748f4..9b8b7d439 100644 --- a/macos/Sources/Lithe/Views/App/WelcomeView.swift +++ b/macos/Sources/Lithe/Views/App/WelcomeView.swift @@ -9,6 +9,7 @@ struct WelcomeView: View { @State private var projectFilter = "" @State private var hoveredProjectID: String? @State private var hoveredProjectMenuID: String? + @State private var showingStableRollback = false @FocusState private var searchFocused: Bool // JetBrains/intellij-community: platform/platform-resources/src/themes/islands/ManyIslandsDark.theme.json @@ -31,6 +32,15 @@ struct WelcomeView: View { } .background(surface.ignoresSafeArea(.container, edges: .top)) .background(WelcomeInitialFocusReset()) + .sheet(isPresented: $showingStableRollback) { + VStack(alignment: .leading, spacing: 16) { + Text("Return to Stable").font(.headline) + StableRollbackControl() + Button("Close") { showingStableRollback = false } + } + .padding(20) + .frame(width: 420) + } } private var welcomeSidebar: some View { @@ -266,9 +276,22 @@ struct WelcomeView: View { .action("Settings…", action: { model.showSettings() }), .separator ] - if case .available(let version, _) = updateChecker.status { + switch updateChecker.status { + case .available(let version, _): items.append(.action("Update to \(version)…", action: { updateChecker.presentDetails() })) - } else { + case .waitingForTermination: + items.append(.action("Continue Installation", action: { + Task { await updateChecker.retryInstallation() } + })) + case .checking: + items.append(.action("Checking for Updates…", isEnabled: false, action: {})) + case .downloading: + items.append(.action("Downloading Update…", isEnabled: false, action: {})) + case .installing: + items.append(.action("Installing Update…", isEnabled: false, action: {})) + case .failed where updateChecker.updateInfo != nil: + items.append(.action("Retry Update…", action: { updateChecker.presentDetails() })) + case .idle, .upToDate, .failed: items.append(.action( "Check for Updates…", isEnabled: !updateChecker.isBusy, @@ -277,6 +300,9 @@ struct WelcomeView: View { } )) } + if updateChecker.isPreview { + items.append(.action("Return to Stable…", action: { showingStableRollback = true })) + } LitheContextMenuPresenter.shared.show( items: items, at: screenPoint,