From 09f9b8b88b88fe12f9c588328dd80f596f94f752 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 00:26:18 +0300 Subject: [PATCH 01/21] refactor: remove favicon from search engine capsule, use solid colors only --- ora/Features/Launcher/LauncherView.swift | 2 +- ora/Features/Launcher/Main/SearchCapsule.swift | 12 +++--------- ora/Features/Search/Models/SearchEngine.swift | 17 +---------------- 3 files changed, 5 insertions(+), 26 deletions(-) diff --git a/ora/Features/Launcher/LauncherView.swift b/ora/Features/Launcher/LauncherView.swift index a574411f..2dbdc70b 100644 --- a/ora/Features/Launcher/LauncherView.swift +++ b/ora/Features/Launcher/LauncherView.swift @@ -85,7 +85,7 @@ struct LauncherView: View { onSubmit: onSubmit ) .gradientAnimatingBorder( - color: match?.faviconBackgroundColor ?? match?.color ?? .clear, + color: match?.color ?? .clear, trigger: match != nil ) .padding(.horizontal, 20) // Add horizontal margins around the search bar diff --git a/ora/Features/Launcher/Main/SearchCapsule.swift b/ora/Features/Launcher/Main/SearchCapsule.swift index a997360f..8eaefc0f 100644 --- a/ora/Features/Launcher/Main/SearchCapsule.swift +++ b/ora/Features/Launcher/Main/SearchCapsule.swift @@ -1,4 +1,3 @@ -import AppKit import SwiftUI struct SearchEngineCapsule: View { @@ -6,22 +5,17 @@ struct SearchEngineCapsule: View { let color: Color let foregroundColor: Color let icon: String - let favicon: NSImage? - let faviconBackgroundColor: Color? var body: some View { HStack(alignment: .center, spacing: 8) { - if let favicon { - Image(nsImage: favicon) - .resizable() - .frame(width: 16, height: 16) - } else if icon.isEmpty { + if icon.isEmpty { Image(systemName: "magnifyingglass") .resizable() .frame(width: 16, height: 16) .foregroundStyle(foregroundColor) } else { Image(icon) + .renderingMode(.template) .resizable() .frame(width: 16, height: 16) .foregroundStyle(foregroundColor) @@ -34,7 +28,7 @@ struct SearchEngineCapsule: View { .padding(.vertical, 6) .padding(.horizontal, 12) .frame(alignment: .leading) - .background(faviconBackgroundColor ?? color) + .background(color) .cornerRadius(99) } } diff --git a/ora/Features/Search/Models/SearchEngine.swift b/ora/Features/Search/Models/SearchEngine.swift index a04351bf..1290fccc 100644 --- a/ora/Features/Search/Models/SearchEngine.swift +++ b/ora/Features/Search/Models/SearchEngine.swift @@ -36,28 +36,13 @@ extension SearchEngine { originalAlias: String, customEngine: CustomSearchEngine? = nil ) -> LauncherMain.Match { - var favicon: NSImage? - var faviconColor: Color? - - // Use cached favicon data from custom engine if available - if let customEngine { - favicon = customEngine.favicon - faviconColor = customEngine.faviconBackgroundColor - } else { - // For built-in engines, use favicon service - favicon = FaviconService.shared.getFavicon(for: searchURL) - faviconColor = FaviconService.shared.getFaviconColor(for: searchURL) - } - return LauncherMain.Match( text: name, color: color, foregroundColor: foregroundColor ?? .white, icon: icon, originalAlias: originalAlias, - searchURL: searchURL, - favicon: favicon, - faviconBackgroundColor: faviconColor + searchURL: searchURL ) } } From adbf3c2e88fe64209177bec8470bf1ce886f6039 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 00:26:23 +0300 Subject: [PATCH 02/21] refactor: remove favicon from launcher Match struct --- ora/Features/Launcher/Main/LauncherMain.swift | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/ora/Features/Launcher/Main/LauncherMain.swift b/ora/Features/Launcher/Main/LauncherMain.swift index 10928710..ebb0d4ba 100644 --- a/ora/Features/Launcher/Main/LauncherMain.swift +++ b/ora/Features/Launcher/Main/LauncherMain.swift @@ -33,8 +33,6 @@ struct LauncherMain: View { let icon: String let originalAlias: String let searchURL: String - let favicon: NSImage? - let faviconBackgroundColor: Color? } @Binding var text: String @@ -68,14 +66,13 @@ struct LauncherMain: View { ) } - _ = FaviconService.shared.getFavicon(for: engine.searchURL) - let faviconURL = FaviconService.shared.faviconURL(forSearchURL: engine.searchURL) - return LauncherSuggestion( type: .aiChat, title: query ?? engine.name, name: engine.name, - faviconURL: faviconURL, + icon: engine.icon.isEmpty ? nil : engine.icon, + color: engine.color, + engineForegroundColor: engine.foregroundColor, action: { tabManager.openFromEngine( engineName: engineName, @@ -295,9 +292,7 @@ struct LauncherMain: View { text: match?.text ?? "", color: match?.color ?? .blue, foregroundColor: match?.foregroundColor ?? .white, - icon: match?.icon ?? "", - favicon: match?.favicon, - faviconBackgroundColor: match?.faviconBackgroundColor + icon: match?.icon ?? "" ) } LauncherTextField( @@ -321,8 +316,7 @@ struct LauncherMain: View { onMoveDown: { moveFocusedElement(.down) }, - cursorColor: match?.faviconBackgroundColor ?? match?.color - ?? (theme.foreground), + cursorColor: match?.color ?? theme.foreground, placeholder: getPlaceholder(match: match) ) .onChange(of: text) { _, _ in @@ -353,7 +347,7 @@ struct LauncherMain: View { RoundedRectangle(cornerRadius: 16, style: .continuous) .inset(by: 0.25) .stroke( - Color(match?.faviconBackgroundColor ?? match?.color ?? theme.foreground) + Color(match?.color ?? theme.foreground) .opacity(0.15), lineWidth: 0.5 ) From 027a97c0520f4d63d07be98631dedeac9e46a50c Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 00:26:29 +0300 Subject: [PATCH 03/21] feat: use per-engine icon and color in AI suggestions instead of default AI --- .../Suggestions/LauncherSuggestionItem.swift | 60 +++++++------------ .../Suggestions/LauncherSuggestionsView.swift | 8 --- 2 files changed, 22 insertions(+), 46 deletions(-) diff --git a/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift b/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift index 992df419..64aaa2ef 100644 --- a/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift +++ b/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift @@ -11,6 +11,8 @@ struct LauncherSuggestion: Identifiable { let name: String? let url: URL? let icon: String? + let color: Color? + let engineForegroundColor: Color? let faviconURL: URL? let faviconLocalFile: URL? let action: () -> Void @@ -21,6 +23,8 @@ struct LauncherSuggestion: Identifiable { name: String? = nil, url: URL? = nil, icon: String? = nil, + color: Color? = nil, + engineForegroundColor: Color? = nil, faviconURL: URL? = nil, faviconLocalFile: URL? = nil, action: @escaping () -> Void @@ -30,6 +34,8 @@ struct LauncherSuggestion: Identifiable { self.name = name self.url = url self.icon = icon + self.color = color + self.engineForegroundColor = engineForegroundColor self.faviconURL = faviconURL self.faviconLocalFile = faviconLocalFile self.action = action @@ -38,7 +44,6 @@ struct LauncherSuggestion: Identifiable { struct LauncherSuggestionItem: View { let suggestion: LauncherSuggestion - let defaultAI: SearchEngine? @Binding var focusedElement: UUID @State private var isHovered = false @@ -46,12 +51,6 @@ struct LauncherSuggestionItem: View { @EnvironmentObject var appState: AppState @EnvironmentObject var toolbarManager: ToolbarManager - init(suggestion: LauncherSuggestion, defaultAI: SearchEngine?, focusedElement: Binding) { - self.suggestion = suggestion - self.defaultAI = defaultAI - self._focusedElement = focusedElement - } - private var isAIChat: Bool { suggestion.type == .aiChat } @@ -60,10 +59,12 @@ struct LauncherSuggestionItem: View { suggestion.url != nil && !isAIChat && suggestion.type != .suggestedQuery && suggestion.type != .openedTab } + private var isFocusedOrHovered: Bool { + focusedElement == suggestion.id || isHovered + } + private var foregroundColor: Color { - if focusedElement == suggestion.id || isHovered, isAIChat { - return defaultAI?.foregroundColor ?? .secondary - } else if focusedElement == suggestion.id { + if focusedElement == suggestion.id { return theme.foreground } return .secondary @@ -71,26 +72,15 @@ struct LauncherSuggestionItem: View { private var backgroundColor: Color { if focusedElement != suggestion.id || isHovered { return .clear } - return isAIChat - ? defaultAI?.color ?? .clear - : isHovered ? theme.foreground.opacity(0.07) : theme.foreground.opacity(0.1) - } - - private var aiIcon: String { - guard isAIChat && defaultAI?.icon != nil else { return "" } - return focusedElement == suggestion.id || isHovered - ? defaultAI!.icon - : defaultAI!.icon + "-inverted" + return isAIChat ? theme.background : theme.foreground.opacity(0.1) } @ViewBuilder var icon: some View { - if isAIChat, defaultAI?.icon != nil { - Image( - aiIcon - ) - .resizable() - .frame(width: 14, height: 14) + if isAIChat, let suggestionIcon = suggestion.icon, !suggestionIcon.isEmpty { + Image(suggestionIcon) + .resizable() + .frame(width: 14, height: 14) } else if suggestion.faviconURL != nil { FavIcon( isWebViewReady: true, @@ -113,27 +103,22 @@ struct LauncherSuggestionItem: View { var actionLabel: some View { if isAIChat { HStack(alignment: .center, spacing: 10) { - Text("Ask \(suggestion.name ?? defaultAI?.name ?? "") ↩") + Text("Ask \(suggestion.name ?? "") ↩") .font(.system(size: 12, weight: .medium)) .foregroundStyle( - focusedElement == suggestion.id || isHovered - ? defaultAI?.foregroundColor ?? .secondary : .secondary + isFocusedOrHovered ? theme.foreground : .secondary ) } .padding(.horizontal, 8) .padding(.vertical, 4) - .background( - focusedElement == suggestion.id || isHovered - ? defaultAI?.foregroundColor?.opacity(0.10) ?? .clear : theme.foreground.opacity(0.07) - ) + .background(theme.foreground.opacity(0.07)) .cornerRadius(6) } else if suggestion.type == .openedTab { HStack(alignment: .center, spacing: 8) { Text("Switch to tab ") .font(.system(size: 12, weight: .medium)) .foregroundStyle( - focusedElement == suggestion.id || isHovered - ? theme.foreground : .secondary + isFocusedOrHovered ? theme.foreground : .secondary ) Image(systemName: "arrow.right") @@ -143,13 +128,12 @@ struct LauncherSuggestionItem: View { .background( RoundedRectangle(cornerRadius: 6, style: .continuous) .fill( - focusedElement == suggestion.id || isHovered + isFocusedOrHovered ? theme.foreground : theme.foreground.opacity(0.07) ) ) .foregroundStyle( - focusedElement == suggestion.id || isHovered - ? theme.background : .secondary + isFocusedOrHovered ? theme.background : .secondary ) } // .padding(.horizontal, 8) diff --git a/ora/Features/Launcher/Suggestions/LauncherSuggestionsView.swift b/ora/Features/Launcher/Suggestions/LauncherSuggestionsView.swift index 525658a3..cbfc01e9 100644 --- a/ora/Features/Launcher/Suggestions/LauncherSuggestionsView.swift +++ b/ora/Features/Launcher/Suggestions/LauncherSuggestionsView.swift @@ -7,7 +7,6 @@ enum SuggestionFocus: Hashable { struct LauncherSuggestionsView: View { @Environment(\.theme) private var theme @Binding var text: String - @StateObject private var searchEngineService = SearchEngineService() @Binding var suggestions: [LauncherSuggestion] @Binding var focusedElement: UUID @@ -16,7 +15,6 @@ struct LauncherSuggestionsView: View { ForEach(suggestions) { suggestion in LauncherSuggestionItem( suggestion: suggestion, - defaultAI: searchEngineService.getDefaultAIChat(), focusedElement: $focusedElement ) } @@ -29,11 +27,5 @@ struct LauncherSuggestionsView: View { .foregroundColor(theme.border.opacity(0.5)), alignment: .top ) - .onAppear { - searchEngineService.setTheme(theme) - } - // .onChange(of: theme) { _, newValue in - // searchEngineService.setTheme(newValue) - // } } } From d10d9828eff95a934e29987aabe09b9a60d8c869 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 00:26:36 +0300 Subject: [PATCH 04/21] feat: add capsule logo assets for all search engines --- .../bing-capsule-logo.imageset/Contents.json | 12 ++++ .../bing-capsule-logo.svg | 1 + .../Contents.json | 12 ++++ .../claude-capsule-logo.svg | 7 +++ .../claude-capsule-logo.imageset/claude.png | Bin 0 -> 9718 bytes .../Contents.json | 12 ++++ .../copilot-capsule-logo.svg | 42 ++++++++++++++ .../Contents.json | 12 ++++ .../copilot-color.svg | 1 + .../Contents.json | 12 ++++ .../duckduckgo-capsule-logo.svg | 52 ++++++++++++++++++ .../Contents.json | 12 ++++ .../gemini-capsule-logo.svg | 9 +++ .../Contents.json | 12 ++++ .../gemini-color.png | Bin 0 -> 19333 bytes .../Contents.json | 12 ++++ .../google-capsule-logo.svg | 7 +++ .../kagi-capsule-logo.imageset/Contents.json | 12 ++++ .../kagi-capsule-logo.svg | 1 + .../x-capsule-logo.imageset/Contents.json | 12 ++++ .../x-capsule-logo.svg | 10 ++++ 21 files changed, 250 insertions(+) create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/bing-capsule-logo.imageset/Contents.json create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/bing-capsule-logo.imageset/bing-capsule-logo.svg create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/Contents.json create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/claude-capsule-logo.svg create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/claude.png create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/copilot-capsule-logo.imageset/Contents.json create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/copilot-capsule-logo.imageset/copilot-capsule-logo.svg create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/copilot-color-capsule-logo.imageset/Contents.json create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/copilot-color-capsule-logo.imageset/copilot-color.svg create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/duckduckgo-capsule-logo.imageset/Contents.json create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/duckduckgo-capsule-logo.imageset/duckduckgo-capsule-logo.svg create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/gemini-capsule-logo.imageset/Contents.json create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/gemini-capsule-logo.imageset/gemini-capsule-logo.svg create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/gemini-color-capsule-logo.imageset/Contents.json create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/gemini-color-capsule-logo.imageset/gemini-color.png create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/google-capsule-logo.imageset/Contents.json create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/google-capsule-logo.imageset/google-capsule-logo.svg create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/kagi-capsule-logo.imageset/Contents.json create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/kagi-capsule-logo.imageset/kagi-capsule-logo.svg create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/x-capsule-logo.imageset/Contents.json create mode 100644 ora/Assets/Catalogs/Capsule.xcassets/x-capsule-logo.imageset/x-capsule-logo.svg diff --git a/ora/Assets/Catalogs/Capsule.xcassets/bing-capsule-logo.imageset/Contents.json b/ora/Assets/Catalogs/Capsule.xcassets/bing-capsule-logo.imageset/Contents.json new file mode 100644 index 00000000..75aac257 --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/bing-capsule-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bing-capsule-logo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ora/Assets/Catalogs/Capsule.xcassets/bing-capsule-logo.imageset/bing-capsule-logo.svg b/ora/Assets/Catalogs/Capsule.xcassets/bing-capsule-logo.imageset/bing-capsule-logo.svg new file mode 100644 index 00000000..0d93e379 --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/bing-capsule-logo.imageset/bing-capsule-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/Contents.json b/ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/Contents.json new file mode 100644 index 00000000..330d0cf8 --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "claude-capsule-logo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/claude-capsule-logo.svg b/ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/claude-capsule-logo.svg new file mode 100644 index 00000000..88968da6 --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/claude-capsule-logo.svg @@ -0,0 +1,7 @@ + + + Claude + + + + diff --git a/ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/claude.png b/ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/claude.png new file mode 100644 index 0000000000000000000000000000000000000000..9d771e3d18e5b4862c7d2b22d06a758a295e81b0 GIT binary patch literal 9718 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 literal 0 HcmV?d00001 diff --git a/ora/Assets/Catalogs/Capsule.xcassets/copilot-capsule-logo.imageset/Contents.json b/ora/Assets/Catalogs/Capsule.xcassets/copilot-capsule-logo.imageset/Contents.json new file mode 100644 index 00000000..ba150758 --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/copilot-capsule-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "copilot-capsule-logo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ora/Assets/Catalogs/Capsule.xcassets/copilot-capsule-logo.imageset/copilot-capsule-logo.svg b/ora/Assets/Catalogs/Capsule.xcassets/copilot-capsule-logo.imageset/copilot-capsule-logo.svg new file mode 100644 index 00000000..78875ee3 --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/copilot-capsule-logo.imageset/copilot-capsule-logo.svg @@ -0,0 +1,42 @@ + + + + + + + + diff --git a/ora/Assets/Catalogs/Capsule.xcassets/copilot-color-capsule-logo.imageset/Contents.json b/ora/Assets/Catalogs/Capsule.xcassets/copilot-color-capsule-logo.imageset/Contents.json new file mode 100644 index 00000000..151637a6 --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/copilot-color-capsule-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "copilot-color.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ora/Assets/Catalogs/Capsule.xcassets/copilot-color-capsule-logo.imageset/copilot-color.svg b/ora/Assets/Catalogs/Capsule.xcassets/copilot-color-capsule-logo.imageset/copilot-color.svg new file mode 100644 index 00000000..4f4031a8 --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/copilot-color-capsule-logo.imageset/copilot-color.svg @@ -0,0 +1 @@ +Copilot \ No newline at end of file diff --git a/ora/Assets/Catalogs/Capsule.xcassets/duckduckgo-capsule-logo.imageset/Contents.json b/ora/Assets/Catalogs/Capsule.xcassets/duckduckgo-capsule-logo.imageset/Contents.json new file mode 100644 index 00000000..03ec690c --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/duckduckgo-capsule-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "duckduckgo-capsule-logo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ora/Assets/Catalogs/Capsule.xcassets/duckduckgo-capsule-logo.imageset/duckduckgo-capsule-logo.svg b/ora/Assets/Catalogs/Capsule.xcassets/duckduckgo-capsule-logo.imageset/duckduckgo-capsule-logo.svg new file mode 100644 index 00000000..415fda00 --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/duckduckgo-capsule-logo.imageset/duckduckgo-capsule-logo.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ora/Assets/Catalogs/Capsule.xcassets/gemini-capsule-logo.imageset/Contents.json b/ora/Assets/Catalogs/Capsule.xcassets/gemini-capsule-logo.imageset/Contents.json new file mode 100644 index 00000000..d97ba10b --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/gemini-capsule-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "gemini-capsule-logo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ora/Assets/Catalogs/Capsule.xcassets/gemini-capsule-logo.imageset/gemini-capsule-logo.svg b/ora/Assets/Catalogs/Capsule.xcassets/gemini-capsule-logo.imageset/gemini-capsule-logo.svg new file mode 100644 index 00000000..a3fc733e --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/gemini-capsule-logo.imageset/gemini-capsule-logo.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/ora/Assets/Catalogs/Capsule.xcassets/gemini-color-capsule-logo.imageset/Contents.json b/ora/Assets/Catalogs/Capsule.xcassets/gemini-color-capsule-logo.imageset/Contents.json new file mode 100644 index 00000000..de59dddb --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/gemini-color-capsule-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "gemini-color.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ora/Assets/Catalogs/Capsule.xcassets/gemini-color-capsule-logo.imageset/gemini-color.png b/ora/Assets/Catalogs/Capsule.xcassets/gemini-color-capsule-logo.imageset/gemini-color.png new file mode 100644 index 0000000000000000000000000000000000000000..e539633a2de4b789b171867a03cda24b97e36bcc GIT binary patch literal 19333 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 literal 0 HcmV?d00001 diff --git a/ora/Assets/Catalogs/Capsule.xcassets/google-capsule-logo.imageset/Contents.json b/ora/Assets/Catalogs/Capsule.xcassets/google-capsule-logo.imageset/Contents.json new file mode 100644 index 00000000..9b1a0ee6 --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/google-capsule-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "google-capsule-logo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ora/Assets/Catalogs/Capsule.xcassets/google-capsule-logo.imageset/google-capsule-logo.svg b/ora/Assets/Catalogs/Capsule.xcassets/google-capsule-logo.imageset/google-capsule-logo.svg new file mode 100644 index 00000000..cf464af1 --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/google-capsule-logo.imageset/google-capsule-logo.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ora/Assets/Catalogs/Capsule.xcassets/kagi-capsule-logo.imageset/Contents.json b/ora/Assets/Catalogs/Capsule.xcassets/kagi-capsule-logo.imageset/Contents.json new file mode 100644 index 00000000..cae69d1a --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/kagi-capsule-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "kagi-capsule-logo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ora/Assets/Catalogs/Capsule.xcassets/kagi-capsule-logo.imageset/kagi-capsule-logo.svg b/ora/Assets/Catalogs/Capsule.xcassets/kagi-capsule-logo.imageset/kagi-capsule-logo.svg new file mode 100644 index 00000000..6e52ecb6 --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/kagi-capsule-logo.imageset/kagi-capsule-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ora/Assets/Catalogs/Capsule.xcassets/x-capsule-logo.imageset/Contents.json b/ora/Assets/Catalogs/Capsule.xcassets/x-capsule-logo.imageset/Contents.json new file mode 100644 index 00000000..b2589a7a --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/x-capsule-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "x-capsule-logo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ora/Assets/Catalogs/Capsule.xcassets/x-capsule-logo.imageset/x-capsule-logo.svg b/ora/Assets/Catalogs/Capsule.xcassets/x-capsule-logo.imageset/x-capsule-logo.svg new file mode 100644 index 00000000..1c16ee3b --- /dev/null +++ b/ora/Assets/Catalogs/Capsule.xcassets/x-capsule-logo.imageset/x-capsule-logo.svg @@ -0,0 +1,10 @@ + + + + + + + From 49121b5f5ea43231eb600527fc044c5e4724329a Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 00:26:41 +0300 Subject: [PATCH 05/21] feat: assign capsule logos to all built-in search engines --- .../Search/Services/SearchEngineService.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ora/Features/Search/Services/SearchEngineService.swift b/ora/Features/Search/Services/SearchEngineService.swift index 708b4283..9628a9b1 100644 --- a/ora/Features/Search/Services/SearchEngineService.swift +++ b/ora/Features/Search/Services/SearchEngineService.swift @@ -81,7 +81,7 @@ class SearchEngineService: ObservableObject { SearchEngine( name: "Claude", color: Color(hex: "#DE7C4C"), - icon: "", + icon: "claude-capsule-logo", aliases: ["claude", "cl", "cla", "anthropic"], searchURL: "https://claude.ai?q={query}", isAIChat: true @@ -89,7 +89,7 @@ class SearchEngineService: ObservableObject { SearchEngine( name: "Google", color: .blue, - icon: "", + icon: "google-capsule-logo", aliases: ["google", "goo", "g", "search"], searchURL: "https://www.google.com/search?client=safari&rls=en&ie=UTF-8&oe=UTF-8&q={query}", @@ -99,7 +99,7 @@ class SearchEngineService: ObservableObject { SearchEngine( name: "DuckDuckGo", color: Color(hex: "#DE5833"), - icon: "", + icon: "duckduckgo-capsule-logo", aliases: ["duckduckgo", "ddg", "duck"], searchURL: "https://duckduckgo.com/?q={query}", isAIChat: false @@ -107,7 +107,7 @@ class SearchEngineService: ObservableObject { SearchEngine( name: "Kagi", color: Color(hex: "#FFB319"), - icon: "", + icon: "kagi-capsule-logo", aliases: ["kagi", "kg"], searchURL: "https://kagi.com/search?q={query}", isAIChat: false @@ -115,7 +115,7 @@ class SearchEngineService: ObservableObject { SearchEngine( name: "Bing", color: Color(hex: "#02B7E9"), - icon: "", + icon: "bing-capsule-logo", aliases: ["bing", "b", "microsoft"], searchURL: "https://www.bing.com/search?q={query}", isAIChat: false @@ -156,7 +156,7 @@ class SearchEngineService: ObservableObject { SearchEngine( name: "X", color: theme?.foreground ?? .white, - icon: "", + icon: "x-capsule-logo", aliases: ["x", "x.com", "twitter", "tw", "twtr", "twit", "twitt", "twitte"], searchURL: "https://twitter.com/search?q={query}", isAIChat: false, @@ -165,7 +165,7 @@ class SearchEngineService: ObservableObject { SearchEngine( name: "Gemini", color: Color(hex: "#4285F4"), - icon: "", + icon: "gemini-color-capsule-logo", aliases: ["gemini", "gem", "bard", "google ai", "gai"], searchURL: "https://gemini.google.com/app?q={query}", isAIChat: true @@ -173,7 +173,7 @@ class SearchEngineService: ObservableObject { SearchEngine( name: "Copilot", color: Color(hex: "#0078D4"), - icon: "", + icon: "copilot-color-capsule-logo", aliases: ["copilot", "microsoft copilot", "bing chat", "bing", "ms copilot"], searchURL: "https://copilot.microsoft.com/?q={query}", isAIChat: true @@ -181,7 +181,7 @@ class SearchEngineService: ObservableObject { SearchEngine( name: "GitHub Copilot", color: Color(hex: "#24292F"), - icon: "", + icon: "copilot-color-capsule-logo", aliases: ["github copilot", "gh copilot", "github ai", "ghc"], searchURL: "https://github.com/copilot?q={query}", isAIChat: true, From 833e1a8eff5fb7c25627c07ce297d7cae5249a54 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 00:26:46 +0300 Subject: [PATCH 06/21] fix: invert OpenAI and Grok logo light/dark variants for AI suggestions --- .../Capsule.xcassets/grok-capsule-logo.imageset/Contents.json | 4 ++-- .../openai-capsule-logo.imageset/Contents.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ora/Assets/Catalogs/Capsule.xcassets/grok-capsule-logo.imageset/Contents.json b/ora/Assets/Catalogs/Capsule.xcassets/grok-capsule-logo.imageset/Contents.json index d179dcd0..dbf7dd1c 100644 --- a/ora/Assets/Catalogs/Capsule.xcassets/grok-capsule-logo.imageset/Contents.json +++ b/ora/Assets/Catalogs/Capsule.xcassets/grok-capsule-logo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "grok-white-capsule-logo.svg", + "filename" : "grok-black-capsule-logo.svg", "idiom" : "universal" }, { @@ -11,7 +11,7 @@ "value" : "dark" } ], - "filename" : "grok-black-capsule-logo.svg", + "filename" : "grok-white-capsule-logo.svg", "idiom" : "universal" } ], diff --git a/ora/Assets/Catalogs/Capsule.xcassets/openai-capsule-logo.imageset/Contents.json b/ora/Assets/Catalogs/Capsule.xcassets/openai-capsule-logo.imageset/Contents.json index 7488035c..0eabd4e8 100644 --- a/ora/Assets/Catalogs/Capsule.xcassets/openai-capsule-logo.imageset/Contents.json +++ b/ora/Assets/Catalogs/Capsule.xcassets/openai-capsule-logo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "openai-white-capsule-logo 1.svg", + "filename" : "opeai-black-capsule-logo.svg", "idiom" : "universal" }, { @@ -11,7 +11,7 @@ "value" : "dark" } ], - "filename" : "opeai-black-capsule-logo.svg", + "filename" : "openai-white-capsule-logo 1.svg", "idiom" : "universal" } ], From 4b0960e31cfb6eb889bdfc8ca6e0f36735c79b49 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 00:34:08 +0300 Subject: [PATCH 07/21] fix: ignore hover on launcher suggestions until mouse moves --- ora/Features/Launcher/LauncherView.swift | 29 +++++++++++++++++++ .../Suggestions/LauncherSuggestionItem.swift | 3 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ora/Features/Launcher/LauncherView.swift b/ora/Features/Launcher/LauncherView.swift index 2dbdc70b..34976cff 100644 --- a/ora/Features/Launcher/LauncherView.swift +++ b/ora/Features/Launcher/LauncherView.swift @@ -1,6 +1,17 @@ import AppKit import SwiftUI +private struct LauncherMouseHasMovedKey: EnvironmentKey { + static let defaultValue: Bool = false +} + +extension EnvironmentValues { + var launcherMouseHasMoved: Bool { + get { self[LauncherMouseHasMovedKey.self] } + set { self[LauncherMouseHasMovedKey.self] = newValue } + } +} + struct LauncherView: View { @EnvironmentObject var appState: AppState @EnvironmentObject var toolbarManager: ToolbarManager @@ -15,6 +26,8 @@ struct LauncherView: View { @State private var isVisible = false @FocusState private var isTextFieldFocused: Bool @State private var match: LauncherMain.Match? + @State private var mouseHasMoved = false + @State private var mouseMonitor: Any? var clearOverlay: Bool? = false @@ -98,12 +111,28 @@ struct LauncherView: View { isVisible = true isTextFieldFocused = true searchEngineService.setTheme(theme) + mouseHasMoved = false + mouseMonitor = NSEvent.addLocalMonitorForEvents(matching: .mouseMoved) { event in + mouseHasMoved = true + if let monitor = mouseMonitor { + NSEvent.removeMonitor(monitor) + mouseMonitor = nil + } + return event + } + } + .onDisappear { + if let monitor = mouseMonitor { + NSEvent.removeMonitor(monitor) + mouseMonitor = nil + } } .onChange(of: appState.showLauncher) { _, newValue in isVisible = newValue } } .frame(maxWidth: .infinity, maxHeight: .infinity) + .environment(\.launcherMouseHasMoved, mouseHasMoved) .onExitCommand { if tabManager.activeTab != nil { isVisible = false diff --git a/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift b/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift index 64aaa2ef..ca3684df 100644 --- a/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift +++ b/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift @@ -48,6 +48,7 @@ struct LauncherSuggestionItem: View { @State private var isHovered = false @Environment(\.theme) private var theme + @Environment(\.launcherMouseHasMoved) private var mouseHasMoved @EnvironmentObject var appState: AppState @EnvironmentObject var toolbarManager: ToolbarManager @@ -175,7 +176,7 @@ struct LauncherSuggestionItem: View { appState.showLauncher = false } .onHover { hover in - if hover { + if hover, mouseHasMoved { focusedElement = suggestion.id } } From e126ada8c79b55565c59eae3ca7e65c083d42d2e Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 00:54:49 +0300 Subject: [PATCH 08/21] refactor: extract models, ViewModel, and environment types from Launcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move LauncherMain.Match → LauncherMatch, LauncherSuggestionType/LauncherSuggestion into dedicated model files. Extract search/suggestion business logic into LauncherViewModel, making LauncherMain a pure view (~114 lines). Consolidate MoveDirection and LauncherMouseHasMovedKey into LauncherEnvironment.swift. Remove duplicate SearchEngineService, unused toolbarManager EnvironmentObject, dead SuggestionFocus enum, and commented-out code. No behavior changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Launcher/LauncherEnvironment.swift | 17 + ora/Features/Launcher/LauncherView.swift | 42 ++- ora/Features/Launcher/Main/LauncherMain.swift | 325 +----------------- .../Launcher/Models/LauncherMatch.swift | 10 + .../Launcher/Models/LauncherSuggestion.swift | 43 +++ .../Launcher/State/LauncherViewModel.swift | 312 +++++++++++++++++ .../Suggestions/LauncherSuggestionItem.swift | 47 --- .../Suggestions/LauncherSuggestionsView.swift | 4 - ora/Features/Search/Models/SearchEngine.swift | 4 +- .../Search/Services/SearchEngineService.swift | 2 +- 10 files changed, 423 insertions(+), 383 deletions(-) create mode 100644 ora/Features/Launcher/LauncherEnvironment.swift create mode 100644 ora/Features/Launcher/Models/LauncherMatch.swift create mode 100644 ora/Features/Launcher/Models/LauncherSuggestion.swift create mode 100644 ora/Features/Launcher/State/LauncherViewModel.swift diff --git a/ora/Features/Launcher/LauncherEnvironment.swift b/ora/Features/Launcher/LauncherEnvironment.swift new file mode 100644 index 00000000..b311741c --- /dev/null +++ b/ora/Features/Launcher/LauncherEnvironment.swift @@ -0,0 +1,17 @@ +import SwiftUI + +enum MoveDirection { + case up + case down +} + +struct LauncherMouseHasMovedKey: EnvironmentKey { + static let defaultValue: Bool = false +} + +extension EnvironmentValues { + var launcherMouseHasMoved: Bool { + get { self[LauncherMouseHasMovedKey.self] } + set { self[LauncherMouseHasMovedKey.self] = newValue } + } +} diff --git a/ora/Features/Launcher/LauncherView.swift b/ora/Features/Launcher/LauncherView.swift index 34976cff..332d2966 100644 --- a/ora/Features/Launcher/LauncherView.swift +++ b/ora/Features/Launcher/LauncherView.swift @@ -1,31 +1,20 @@ import AppKit import SwiftUI -private struct LauncherMouseHasMovedKey: EnvironmentKey { - static let defaultValue: Bool = false -} - -extension EnvironmentValues { - var launcherMouseHasMoved: Bool { - get { self[LauncherMouseHasMovedKey.self] } - set { self[LauncherMouseHasMovedKey.self] = newValue } - } -} - struct LauncherView: View { @EnvironmentObject var appState: AppState - @EnvironmentObject var toolbarManager: ToolbarManager @EnvironmentObject var tabManager: TabManager @EnvironmentObject var historyManager: HistoryManager @EnvironmentObject var downloadManager: DownloadManager @EnvironmentObject var privacyMode: PrivacyMode @Environment(\.theme) private var theme - @StateObject private var searchEngineService = SearchEngineService() + + @StateObject private var viewModel = LauncherViewModel() @State private var input = "" @State private var isVisible = false @FocusState private var isTextFieldFocused: Bool - @State private var match: LauncherMain.Match? + @State private var match: LauncherMatch? @State private var mouseHasMoved = false @State private var mouseMonitor: Any? @@ -33,8 +22,8 @@ struct LauncherView: View { private func onTabPress() { guard !input.isEmpty else { return } - if let searchEngine = searchEngineService.findSearchEngine(for: input) { - let customEngine = searchEngineService.settings.customSearchEngines + if let searchEngine = viewModel.searchEngineService.findSearchEngine(for: input) { + let customEngine = viewModel.searchEngineService.settings.customSearchEngines .first { $0.searchURL == searchEngine.searchURL } match = searchEngine.toLauncherMatch( originalAlias: input, @@ -49,11 +38,11 @@ struct LauncherView: View { var engineToUse = match if engineToUse == nil, - let defaultEngine = searchEngineService.getDefaultSearchEngine( + let defaultEngine = viewModel.searchEngineService.getDefaultSearchEngine( for: tabManager.activeContainer?.id ) { - let customEngine = searchEngineService.settings.customSearchEngines + let customEngine = viewModel.searchEngineService.settings.customSearchEngines .first { $0.searchURL == defaultEngine.searchURL } engineToUse = defaultEngine.toLauncherMatch( originalAlias: correctInput, @@ -62,7 +51,7 @@ struct LauncherView: View { } if let engine = engineToUse, - let url = searchEngineService.createSearchURL(for: engine, query: correctInput) + let url = viewModel.searchEngineService.createSearchURL(for: engine, query: correctInput) { tabManager .openTab( @@ -95,13 +84,14 @@ struct LauncherView: View { match: $match, isFocused: $isTextFieldFocused, onTabPress: onTabPress, - onSubmit: onSubmit + onSubmit: onSubmit, + viewModel: viewModel ) .gradientAnimatingBorder( color: match?.color ?? .clear, trigger: match != nil ) - .padding(.horizontal, 20) // Add horizontal margins around the search bar + .padding(.horizontal, 20) .offset(y: 250) .scaleEffect(isVisible ? 1.0 : 0.9) .opacity(isVisible ? 1.0 : 0.0) @@ -110,7 +100,15 @@ struct LauncherView: View { .onAppear { isVisible = true isTextFieldFocused = true - searchEngineService.setTheme(theme) + viewModel.searchEngineService.setTheme(theme) + viewModel.configure( + tabManager: tabManager, + historyManager: historyManager, + downloadManager: downloadManager, + appState: appState, + privacyMode: privacyMode, + onSubmit: onSubmit + ) mouseHasMoved = false mouseMonitor = NSEvent.addLocalMonitorForEvents(matching: .mouseMoved) { event in mouseHasMoved = true diff --git a/ora/Features/Launcher/Main/LauncherMain.swift b/ora/Features/Launcher/Main/LauncherMain.swift index ebb0d4ba..b2a32224 100644 --- a/ora/Features/Launcher/Main/LauncherMain.swift +++ b/ora/Features/Launcher/Main/LauncherMain.swift @@ -1,281 +1,14 @@ import SwiftUI -enum MoveDirection { - case up - case down -} - -class Debouncer { - private var workItem: DispatchWorkItem? - private let delay: TimeInterval - - init(delay: TimeInterval) { - self.delay = delay - } - - func run(_ block: @escaping @Sendable () async -> Void) { - workItem?.cancel() - let item = DispatchWorkItem { - Task { await block() } - } - workItem = item - DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: item) - } -} - -let debouncer = Debouncer(delay: 0.2) - struct LauncherMain: View { - struct Match { - let text: String - let color: Color - let foregroundColor: Color - let icon: String - let originalAlias: String - let searchURL: String - } - @Binding var text: String - @Binding var match: Match? + @Binding var match: LauncherMatch? var isFocused: FocusState.Binding let onTabPress: () -> Void let onSubmit: (String?) -> Void + @ObservedObject var viewModel: LauncherViewModel @Environment(\.theme) private var theme - @EnvironmentObject var historyManager: HistoryManager - @EnvironmentObject var downloadManager: DownloadManager - @EnvironmentObject var tabManager: TabManager - @EnvironmentObject var appState: AppState - @EnvironmentObject var toolbarManager: ToolbarManager - @EnvironmentObject var privacyMode: PrivacyMode - @State var focusedElement: UUID = .init() - - @StateObject private var searchEngineService = SearchEngineService() - - @State private var suggestions: [LauncherSuggestion] = [] - - private func createAISuggestion(engineName: SearchEngineID, query: String? = nil) - -> LauncherSuggestion - { - guard let engine = searchEngineService.getSearchEngine(engineName) else { - return LauncherSuggestion( - type: .aiChat, - title: query ?? engineName.rawValue, - name: engineName.rawValue, - action: {} - ) - } - - return LauncherSuggestion( - type: .aiChat, - title: query ?? engine.name, - name: engine.name, - icon: engine.icon.isEmpty ? nil : engine.icon, - color: engine.color, - engineForegroundColor: engine.foregroundColor, - action: { - tabManager.openFromEngine( - engineName: engineName, - query: query ?? text, - historyManager: historyManager, - isPrivate: privacyMode.isPrivate - ) - } - ) - } - - func defaultSuggestions() -> [LauncherSuggestion] { - let containerId = tabManager.activeContainer?.id - let searchEngine = searchEngineService.getDefaultSearchEngine(for: containerId) - let engineName = searchEngine?.name ?? "Google" - return [ - LauncherSuggestion( - type: .suggestedQuery, title: "Search on \(engineName)", - action: { onSubmit(nil) } - ), - createAISuggestion(engineName: .grok), - createAISuggestion(engineName: .chatgpt), - createAISuggestion(engineName: .claude), - createAISuggestion(engineName: .gemini) - ] - } - - private func isValidHostname(_ input: String) -> Bool { - let regex = #"^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$"# - return input.range(of: regex, options: .regularExpression) != nil - } - - func searchHandler(_ text: String) { - guard !text.trimmingCharacters(in: .whitespaces).isEmpty else { - suggestions = defaultSuggestions() - return - } - - let histories = historyManager.search( - text, - activeContainerId: tabManager.activeContainer?.id ?? UUID() - ) - let tabs = tabManager.search(text) - - suggestions = [] - - var itemsCount = 0 - appendOpenTabs(tabs, itemsCount: &itemsCount) - appendOpenURLSuggestionIfNeeded(text) - appendSearchWithDefaultEngineSuggestion(text) - - let insertIndex = suggestions.count - requestAutoSuggestions(text, insertAt: insertIndex) - - appendHistorySuggestions(histories, itemsCount: &itemsCount) - appendAISuggestionsIfNeeded(text) - - focusedElement = suggestions.first?.id ?? UUID() - } - - private func appendOpenTabs(_ tabs: [Tab], itemsCount: inout Int) { - for tab in tabs { - if itemsCount >= 2 { break } - suggestions.append( - LauncherSuggestion( - type: .openedTab, - title: tab.title, - url: tab.url, - faviconURL: tab.favicon, - faviconLocalFile: tab.faviconLocalFile, - action: { - if !tab.isWebViewReady { - tab.restoreTransientState( - historyManager: historyManager, - downloadManager: downloadManager, - tabManager: tabManager, - isPrivate: privacyMode.isPrivate - ) - } - tabManager.activateTab(tab) - } - ) - ) - itemsCount += 1 - } - } - - private func appendOpenURLSuggestionIfNeeded(_ text: String) { - guard let candidateURL = URL(string: text) else { return } - let finalURL: URL? = - if candidateURL.scheme != nil { - candidateURL - } else if isValidURL(text) { - constructURL(from: text) - } else { - nil - } - guard let url = finalURL else { return } - suggestions.append( - LauncherSuggestion( - type: .suggestedLink, - title: text, - url: url, - action: { - tabManager.openTab( - url: url, - historyManager: historyManager, - downloadManager: downloadManager, - isPrivate: privacyMode.isPrivate - ) - } - ) - ) - } - - private func appendSearchWithDefaultEngineSuggestion(_ text: String) { - let containerId = tabManager.activeContainer?.id - let searchEngine = searchEngineService.getDefaultSearchEngine(for: containerId) - let engineName = searchEngine?.name ?? "Google" - suggestions.append( - LauncherSuggestion( - type: .suggestedQuery, - title: "\(text) - Search with \(engineName)", - action: { onSubmit(nil) } - ) - ) - } - - private func requestAutoSuggestions(_ text: String, insertAt: Int) { - let containerId = tabManager.activeContainer?.id - debouncer.run { - let searchEngine = await self.searchEngineService.getDefaultSearchEngine(for: containerId) - if let autoSuggestions = searchEngine?.autoSuggestions { - let searchSuggestions = await autoSuggestions(text) - await MainActor.run { - var localCount = 0 - for ss in searchSuggestions { - if localCount == 3 { break } - let insertIndex = insertAt + localCount - let suggestion = LauncherSuggestion( - type: .suggestedQuery, - title: ss, - action: { onSubmit(ss) } - ) - if insertIndex <= suggestions.count { - suggestions.insert(suggestion, at: insertIndex) - } else { - suggestions.append(suggestion) - } - localCount += 1 - } - } - } - } - } - - private func appendHistorySuggestions(_ histories: [History], itemsCount: inout Int) { - for history in histories { - if itemsCount >= 5 { break } - suggestions.append( - LauncherSuggestion( - type: .suggestedLink, - title: history.title, - url: history.url, - faviconURL: history.faviconURL, - faviconLocalFile: history.faviconLocalFile, - action: { - tabManager.openTab( - url: history.url, - historyManager: historyManager, - isPrivate: privacyMode.isPrivate - ) - } - ) - ) - itemsCount += 1 - } - } - - private func appendAISuggestionsIfNeeded(_ text: String) { - guard isAISuitableQuery(text) else { return } - suggestions.append(createAISuggestion(engineName: .grok, query: text)) - suggestions.append(createAISuggestion(engineName: .chatgpt, query: text)) - suggestions.append(createAISuggestion(engineName: .claude, query: text)) - suggestions.append(createAISuggestion(engineName: .gemini, query: text)) - } - - func executeCommand() { - if let suggestion = - suggestions - .first(where: { $0.id == focusedElement }) - { - suggestion.action() - appState.showLauncher = false - } - } - - func moveFocusedElement(_ dir: MoveDirection) { - guard let idx = suggestions.firstIndex(where: { $0.id == focusedElement }) else { return } - let offset = dir == .up ? -1 : 1 - let newIndex = (idx + offset + suggestions.count) % suggestions.count - focusedElement = suggestions[newIndex].id - } var body: some View { VStack(alignment: .leading, spacing: 6) { @@ -300,27 +33,28 @@ struct LauncherMain: View { font: NSFont.systemFont(ofSize: 18, weight: .medium), onTab: onTabPress, onSubmit: { - executeCommand() + viewModel.executeCommand() }, onDelete: { - if text.isEmpty, match != nil { - text = match!.originalAlias + if text.isEmpty, let currentMatch = match { + text = currentMatch.originalAlias match = nil return true } return false }, onMoveUp: { - moveFocusedElement(.up) + viewModel.moveFocusedElement(.up) }, onMoveDown: { - moveFocusedElement(.down) + viewModel.moveFocusedElement(.down) }, cursorColor: match?.color ?? theme.foreground, placeholder: getPlaceholder(match: match) ) - .onChange(of: text) { _, _ in - searchHandler(text) + .onChange(of: text) { _, newValue in + viewModel.currentText = newValue + viewModel.searchHandler(newValue) } .textFieldStyle(PlainTextFieldStyle()) .focused(isFocused) @@ -330,11 +64,11 @@ struct LauncherMain: View { .padding(.vertical, 10) .frame(maxWidth: .infinity, alignment: .leading) - if match == nil, !suggestions.isEmpty { + if match == nil, !viewModel.suggestions.isEmpty { LauncherSuggestionsView( text: $text, - suggestions: $suggestions, - focusedElement: $focusedElement + suggestions: $viewModel.suggestions, + focusedElement: $viewModel.focusedElement ) } } @@ -358,46 +92,23 @@ struct LauncherMain: View { ) } - private func getPlaceholder(match: Match?) -> String { - if match == nil { + private func getPlaceholder(match: LauncherMatch?) -> String { + guard let match else { return "Search the web or enter url..." } - // Find the search engine by name to get its isAIChat property - if let engine = searchEngineService.getSearchEngine(byName: match!.text) { + if let engine = viewModel.searchEngineService.getSearchEngine(byName: match.text) { let prefix = engine.isAIChat ? "Ask" : "Search on" return "\(prefix) \(engine.name)" } - // Fallback (should rarely happen) - return "Search on \(match!.text)" + return "Search on \(match.text)" } - private func getIconName(match: Match?, text: String) -> String { + private func getIconName(match: LauncherMatch?, text: String) -> String { if match != nil { return "magnifyingglass" } return isValidURL(text) ? "globe" : "magnifyingglass" } } - -func isAISuitableQuery(_ query: String) -> Bool { - let lowercased = query.lowercased() - - // AI-suited queries: open-ended, creative, opinion-based, etc. - let aiKeywords = [ - #"^(who|when|where|what|how|why)\b.*\?$"#, // e.g. "When was Apple founded?" - #"^\d{4}"#, - "summarize", "rewrite", "explain", "code", "how to", "generate", - "idea", "opinion", "feedback", "story", "joke", "email", "draft", - "translate", "compare", "alternatives", "improve", "fix", "suggest" - ] - - for keyword in aiKeywords { - if lowercased.contains(keyword) { - return true - } - } - - return false -} diff --git a/ora/Features/Launcher/Models/LauncherMatch.swift b/ora/Features/Launcher/Models/LauncherMatch.swift new file mode 100644 index 00000000..b53afd33 --- /dev/null +++ b/ora/Features/Launcher/Models/LauncherMatch.swift @@ -0,0 +1,10 @@ +import SwiftUI + +struct LauncherMatch { + let text: String + let color: Color + let foregroundColor: Color + let icon: String + let originalAlias: String + let searchURL: String +} diff --git a/ora/Features/Launcher/Models/LauncherSuggestion.swift b/ora/Features/Launcher/Models/LauncherSuggestion.swift new file mode 100644 index 00000000..1dbe8430 --- /dev/null +++ b/ora/Features/Launcher/Models/LauncherSuggestion.swift @@ -0,0 +1,43 @@ +import SwiftUI + +enum LauncherSuggestionType { + case openedTab, suggestedQuery, suggestedLink, aiChat +} + +struct LauncherSuggestion: Identifiable { + let id = UUID() + let type: LauncherSuggestionType + let title: String + let name: String? + let url: URL? + let icon: String? + let color: Color? + let engineForegroundColor: Color? + let faviconURL: URL? + let faviconLocalFile: URL? + let action: () -> Void + + init( + type: LauncherSuggestionType, + title: String, + name: String? = nil, + url: URL? = nil, + icon: String? = nil, + color: Color? = nil, + engineForegroundColor: Color? = nil, + faviconURL: URL? = nil, + faviconLocalFile: URL? = nil, + action: @escaping () -> Void + ) { + self.type = type + self.title = title + self.name = name + self.url = url + self.icon = icon + self.color = color + self.engineForegroundColor = engineForegroundColor + self.faviconURL = faviconURL + self.faviconLocalFile = faviconLocalFile + self.action = action + } +} diff --git a/ora/Features/Launcher/State/LauncherViewModel.swift b/ora/Features/Launcher/State/LauncherViewModel.swift new file mode 100644 index 00000000..5ffb8f01 --- /dev/null +++ b/ora/Features/Launcher/State/LauncherViewModel.swift @@ -0,0 +1,312 @@ +import SwiftUI + +@MainActor +class LauncherViewModel: ObservableObject { + let searchEngineService = SearchEngineService() + + @Published var suggestions: [LauncherSuggestion] = [] + @Published var focusedElement: UUID = .init() + + /// Kept in sync with the view's text binding so closures can read current input. + var currentText: String = "" + + private let debouncer = Debouncer(delay: 0.2) + + // Dependencies injected from the view layer + private(set) var tabManager: TabManager? + private(set) var historyManager: HistoryManager? + private(set) var downloadManager: DownloadManager? + private(set) var appState: AppState? + private(set) var privacyMode: PrivacyMode? + private(set) var onSubmit: ((String?) -> Void)? + + func configure( + tabManager: TabManager, + historyManager: HistoryManager, + downloadManager: DownloadManager, + appState: AppState, + privacyMode: PrivacyMode, + onSubmit: @escaping (String?) -> Void + ) { + self.tabManager = tabManager + self.historyManager = historyManager + self.downloadManager = downloadManager + self.appState = appState + self.privacyMode = privacyMode + self.onSubmit = onSubmit + } + + // MARK: - Search Logic + + func searchHandler(_ text: String) { + guard let tabManager, let historyManager else { return } + + guard !text.trimmingCharacters(in: .whitespaces).isEmpty else { + suggestions = defaultSuggestions() + return + } + + let histories = historyManager.search( + text, + activeContainerId: tabManager.activeContainer?.id ?? UUID() + ) + let tabs = tabManager.search(text) + + suggestions = [] + + var itemsCount = 0 + appendOpenTabs(tabs, itemsCount: &itemsCount) + appendOpenURLSuggestionIfNeeded(text) + appendSearchWithDefaultEngineSuggestion(text) + + let insertIndex = suggestions.count + requestAutoSuggestions(text, insertAt: insertIndex) + + appendHistorySuggestions(histories, itemsCount: &itemsCount) + appendAISuggestionsIfNeeded(text) + + focusedElement = suggestions.first?.id ?? UUID() + } + + func defaultSuggestions() -> [LauncherSuggestion] { + guard let tabManager else { return [] } + let containerId = tabManager.activeContainer?.id + let searchEngine = searchEngineService.getDefaultSearchEngine(for: containerId) + let engineName = searchEngine?.name ?? "Google" + return [ + LauncherSuggestion( + type: .suggestedQuery, title: "Search on \(engineName)", + action: { [weak self] in self?.onSubmit?(nil) } + ), + createAISuggestion(engineName: .grok), + createAISuggestion(engineName: .chatgpt), + createAISuggestion(engineName: .claude), + createAISuggestion(engineName: .gemini) + ] + } + + func executeCommand() { + if let suggestion = suggestions.first(where: { $0.id == focusedElement }) { + suggestion.action() + appState?.showLauncher = false + } + } + + func moveFocusedElement(_ dir: MoveDirection) { + guard let idx = suggestions.firstIndex(where: { $0.id == focusedElement }) else { return } + let offset = dir == .up ? -1 : 1 + let newIndex = (idx + offset + suggestions.count) % suggestions.count + focusedElement = suggestions[newIndex].id + } + + // MARK: - Private Helpers + + private func createAISuggestion(engineName: SearchEngineID, query: String? = nil) + -> LauncherSuggestion + { + guard let engine = searchEngineService.getSearchEngine(engineName) else { + return LauncherSuggestion( + type: .aiChat, + title: query ?? engineName.rawValue, + name: engineName.rawValue, + action: {} + ) + } + + return LauncherSuggestion( + type: .aiChat, + title: query ?? engine.name, + name: engine.name, + icon: engine.icon.isEmpty ? nil : engine.icon, + color: engine.color, + engineForegroundColor: engine.foregroundColor, + action: { [weak self] in + guard let self, let tabManager = self.tabManager, + let historyManager = self.historyManager, + let privacyMode = self.privacyMode + else { return } + tabManager.openFromEngine( + engineName: engineName, + query: query ?? self.currentText, + historyManager: historyManager, + isPrivate: privacyMode.isPrivate + ) + } + ) + } + + private func appendOpenTabs(_ tabs: [Tab], itemsCount: inout Int) { + guard let tabManager, let historyManager, let downloadManager, let privacyMode else { + return + } + for tab in tabs { + if itemsCount >= 2 { break } + suggestions.append( + LauncherSuggestion( + type: .openedTab, + title: tab.title, + url: tab.url, + faviconURL: tab.favicon, + faviconLocalFile: tab.faviconLocalFile, + action: { + if !tab.isWebViewReady { + tab.restoreTransientState( + historyManager: historyManager, + downloadManager: downloadManager, + tabManager: tabManager, + isPrivate: privacyMode.isPrivate + ) + } + tabManager.activateTab(tab) + } + ) + ) + itemsCount += 1 + } + } + + private func appendOpenURLSuggestionIfNeeded(_ text: String) { + guard let tabManager, let historyManager, let downloadManager, let privacyMode else { + return + } + guard let candidateURL = URL(string: text) else { return } + let finalURL: URL? = + if candidateURL.scheme != nil { + candidateURL + } else if isValidURL(text) { + constructURL(from: text) + } else { + nil + } + guard let url = finalURL else { return } + suggestions.append( + LauncherSuggestion( + type: .suggestedLink, + title: text, + url: url, + action: { + tabManager.openTab( + url: url, + historyManager: historyManager, + downloadManager: downloadManager, + isPrivate: privacyMode.isPrivate + ) + } + ) + ) + } + + private func appendSearchWithDefaultEngineSuggestion(_ text: String) { + guard let tabManager else { return } + let containerId = tabManager.activeContainer?.id + let searchEngine = searchEngineService.getDefaultSearchEngine(for: containerId) + let engineName = searchEngine?.name ?? "Google" + suggestions.append( + LauncherSuggestion( + type: .suggestedQuery, + title: "\(text) - Search with \(engineName)", + action: { [weak self] in self?.onSubmit?(nil) } + ) + ) + } + + private func requestAutoSuggestions(_ text: String, insertAt: Int) { + guard let tabManager else { return } + let containerId = tabManager.activeContainer?.id + debouncer.run { [weak self] in + guard let self else { return } + let searchEngine = await self.searchEngineService.getDefaultSearchEngine( + for: containerId + ) + if let autoSuggestions = searchEngine?.autoSuggestions { + let searchSuggestions = await autoSuggestions(text) + await MainActor.run { + var localCount = 0 + for searchSuggestion in searchSuggestions { + if localCount == 3 { break } + let insertIndex = insertAt + localCount + let suggestion = LauncherSuggestion( + type: .suggestedQuery, + title: searchSuggestion, + action: { [weak self] in self?.onSubmit?(searchSuggestion) } + ) + if insertIndex <= self.suggestions.count { + self.suggestions.insert(suggestion, at: insertIndex) + } else { + self.suggestions.append(suggestion) + } + localCount += 1 + } + } + } + } + } + + private func appendHistorySuggestions(_ histories: [History], itemsCount: inout Int) { + guard let tabManager, let historyManager, let privacyMode else { return } + for history in histories { + if itemsCount >= 5 { break } + suggestions.append( + LauncherSuggestion( + type: .suggestedLink, + title: history.title, + url: history.url, + faviconURL: history.faviconURL, + faviconLocalFile: history.faviconLocalFile, + action: { + tabManager.openTab( + url: history.url, + historyManager: historyManager, + isPrivate: privacyMode.isPrivate + ) + } + ) + ) + itemsCount += 1 + } + } + + private func appendAISuggestionsIfNeeded(_ text: String) { + guard isAISuitableQuery(text) else { return } + suggestions.append(createAISuggestion(engineName: .grok, query: text)) + suggestions.append(createAISuggestion(engineName: .chatgpt, query: text)) + suggestions.append(createAISuggestion(engineName: .claude, query: text)) + suggestions.append(createAISuggestion(engineName: .gemini, query: text)) + } + + private func isAISuitableQuery(_ query: String) -> Bool { + let lowercased = query.lowercased() + + let aiKeywords = [ + #"^(who|when|where|what|how|why)\b.*\?$"#, + #"^\d{4}"#, + "summarize", "rewrite", "explain", "code", "how to", "generate", + "idea", "opinion", "feedback", "story", "joke", "email", "draft", + "translate", "compare", "alternatives", "improve", "fix", "suggest" + ] + + for keyword in aiKeywords where lowercased.contains(keyword) { + return true + } + + return false + } +} + +private class Debouncer { + private var workItem: DispatchWorkItem? + private let delay: TimeInterval + + init(delay: TimeInterval) { + self.delay = delay + } + + func run(_ block: @escaping @Sendable () async -> Void) { + workItem?.cancel() + let item = DispatchWorkItem { + Task { await block() } + } + workItem = item + DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: item) + } +} diff --git a/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift b/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift index ca3684df..89b25c69 100644 --- a/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift +++ b/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift @@ -1,47 +1,5 @@ import SwiftUI -enum LauncherSuggestionType { - case openedTab, suggestedQuery, suggestedLink, aiChat -} - -struct LauncherSuggestion: Identifiable { - let id = UUID() - let type: LauncherSuggestionType - let title: String - let name: String? - let url: URL? - let icon: String? - let color: Color? - let engineForegroundColor: Color? - let faviconURL: URL? - let faviconLocalFile: URL? - let action: () -> Void - - init( - type: LauncherSuggestionType, - title: String, - name: String? = nil, - url: URL? = nil, - icon: String? = nil, - color: Color? = nil, - engineForegroundColor: Color? = nil, - faviconURL: URL? = nil, - faviconLocalFile: URL? = nil, - action: @escaping () -> Void - ) { - self.type = type - self.title = title - self.name = name - self.url = url - self.icon = icon - self.color = color - self.engineForegroundColor = engineForegroundColor - self.faviconURL = faviconURL - self.faviconLocalFile = faviconLocalFile - self.action = action - } -} - struct LauncherSuggestionItem: View { let suggestion: LauncherSuggestion @Binding var focusedElement: UUID @@ -50,7 +8,6 @@ struct LauncherSuggestionItem: View { @Environment(\.theme) private var theme @Environment(\.launcherMouseHasMoved) private var mouseHasMoved @EnvironmentObject var appState: AppState - @EnvironmentObject var toolbarManager: ToolbarManager private var isAIChat: Bool { suggestion.type == .aiChat @@ -137,9 +94,6 @@ struct LauncherSuggestionItem: View { isFocusedOrHovered ? theme.background : .secondary ) } - // .padding(.horizontal, 8) - // .padding(.vertical, 4) - // .background(theme.foreground.opacity(0.07)) .cornerRadius(6) } } @@ -180,6 +134,5 @@ struct LauncherSuggestionItem: View { focusedElement = suggestion.id } } -// .focused($focusedElement, equals: suggestion.id) } } diff --git a/ora/Features/Launcher/Suggestions/LauncherSuggestionsView.swift b/ora/Features/Launcher/Suggestions/LauncherSuggestionsView.swift index cbfc01e9..97d2e464 100644 --- a/ora/Features/Launcher/Suggestions/LauncherSuggestionsView.swift +++ b/ora/Features/Launcher/Suggestions/LauncherSuggestionsView.swift @@ -1,9 +1,5 @@ import SwiftUI -enum SuggestionFocus: Hashable { - case suggestion(id: UUID) -} - struct LauncherSuggestionsView: View { @Environment(\.theme) private var theme @Binding var text: String diff --git a/ora/Features/Search/Models/SearchEngine.swift b/ora/Features/Search/Models/SearchEngine.swift index 1290fccc..a11e587d 100644 --- a/ora/Features/Search/Models/SearchEngine.swift +++ b/ora/Features/Search/Models/SearchEngine.swift @@ -35,8 +35,8 @@ extension SearchEngine { func toLauncherMatch( originalAlias: String, customEngine: CustomSearchEngine? = nil - ) -> LauncherMain.Match { - return LauncherMain.Match( + ) -> LauncherMatch { + return LauncherMatch( text: name, color: color, foregroundColor: foregroundColor ?? .white, diff --git a/ora/Features/Search/Services/SearchEngineService.swift b/ora/Features/Search/Services/SearchEngineService.swift index 9628a9b1..d9a8478a 100644 --- a/ora/Features/Search/Services/SearchEngineService.swift +++ b/ora/Features/Search/Services/SearchEngineService.swift @@ -280,7 +280,7 @@ class SearchEngineService: ObservableObject { return URL(string: urlString) } - func createSearchURL(for match: LauncherMain.Match, query: String) -> URL? { + func createSearchURL(for match: LauncherMatch, query: String) -> URL? { let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" let urlString = match.searchURL.replacingOccurrences(of: "{query}", with: encodedQuery) From ed98c0765c7d69777ba90fce569ae2039f08fe8c Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 19:36:38 +0300 Subject: [PATCH 09/21] fix: support localhost:port and ip:port in launcher URL suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Recognize localhost as a valid host in isValidURL - Use http:// scheme for localhost in constructURL (devs typically use HTTP) - Require both scheme and host in appendOpenURLSuggestionIfNeeded so localhost:3000 (parsed as scheme=localhost, host=nil) falls through to constructURL instead of being used as-is - Fix 192.168.1.1:8080 which URL(string:) returns nil for, now falls through to isValidURL → constructURL correctly Co-Authored-By: Claude Opus 4.6 (1M context) --- ora/Core/Utilities/Utils.swift | 12 ++++++++---- .../Launcher/State/LauncherViewModel.swift | 18 +++++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/ora/Core/Utilities/Utils.swift b/ora/Core/Utilities/Utils.swift index 63ca8df4..3dd36666 100644 --- a/ora/Core/Utilities/Utils.swift +++ b/ora/Core/Utilities/Utils.swift @@ -15,6 +15,10 @@ func extractDomainOrIP(from text: String) -> String? { func isValidURL(_ text: String) -> Bool { guard let host = extractDomainOrIP(from: text) else { return false } + if host == "localhost" { + return true + } + let ipPattern = #"^(\d{1,3}\.){3}\d{1,3}$"# if host.range(of: ipPattern, options: .regularExpression) != nil { return true @@ -31,8 +35,8 @@ func constructURL(from text: String) -> URL? { if trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") { return URL(string: trimmed) } - if isValidURL(trimmed) { - return URL(string: "https://\(trimmed)") - } - return nil + guard isValidURL(trimmed) else { return nil } + let host = extractDomainOrIP(from: trimmed) + let scheme = (host == "localhost") ? "http" : "https" + return URL(string: "\(scheme)://\(trimmed)") } diff --git a/ora/Features/Launcher/State/LauncherViewModel.swift b/ora/Features/Launcher/State/LauncherViewModel.swift index 5ffb8f01..b5708237 100644 --- a/ora/Features/Launcher/State/LauncherViewModel.swift +++ b/ora/Features/Launcher/State/LauncherViewModel.swift @@ -169,15 +169,15 @@ class LauncherViewModel: ObservableObject { guard let tabManager, let historyManager, let downloadManager, let privacyMode else { return } - guard let candidateURL = URL(string: text) else { return } - let finalURL: URL? = - if candidateURL.scheme != nil { - candidateURL - } else if isValidURL(text) { - constructURL(from: text) - } else { - nil - } + let finalURL: URL? = if let candidateURL = URL(string: text), candidateURL.scheme != nil, + candidateURL.host != nil + { + candidateURL + } else if isValidURL(text) { + constructURL(from: text) + } else { + nil + } guard let url = finalURL else { return } suggestions.append( LauncherSuggestion( From 0e9327de9a188140a3d8e41da2b82a5621aba9bf Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 19:36:50 +0300 Subject: [PATCH 10/21] fix: conditional binding on non-optional URL in Tab.continueToInsecureSite self.url is non-optional, so failedURL ?? self.url always produces URL. Use let instead of guard let to fix the build error. Co-Authored-By: Claude Opus 4.6 (1M context) --- ora/Features/Tabs/Models/Tab.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ora/Features/Tabs/Models/Tab.swift b/ora/Features/Tabs/Models/Tab.swift index b734b791..a200bdb9 100644 --- a/ora/Features/Tabs/Models/Tab.swift +++ b/ora/Features/Tabs/Models/Tab.swift @@ -319,6 +319,14 @@ class Tab: ObservableObject, Identifiable { } } + func continueToInsecureSite() { + let url = failedURL ?? self.url + guard let host = url.host else { return } + browserPage?.bypassSSL(for: host) + clearNavigationError() + browserPage?.load(URLRequest(url: url)) + } + var canGoBack: Bool { browserPage?.canGoBack ?? false } From b46d12ce6ac105d1f69e436830f36072612f0f70 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 19:37:21 +0300 Subject: [PATCH 11/21] fix: remove unassigned claude.png from claude-capsule-logo imageset The SVG is the referenced asset; the PNG was orphaned and causing an Xcode "unassigned child" warning. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../claude-capsule-logo.imageset/claude.png | Bin 9718 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/claude.png diff --git a/ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/claude.png b/ora/Assets/Catalogs/Capsule.xcassets/claude-capsule-logo.imageset/claude.png deleted file mode 100644 index 9d771e3d18e5b4862c7d2b22d06a758a295e81b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9718 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 From 2ee9c69780fdf1dca272d7e8d1a757c6102430e7 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 19:37:31 +0300 Subject: [PATCH 12/21] fix: ignore hover on floating tab switcher until mouse moves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same pattern as the launcher fix — track first mouse movement with NSEvent monitor so keyboard-driven focus isn't hijacked by the cursor's resting position when the switcher appears. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Tabs/Switcher/FloatingTabSwitcher.swift | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/ora/Features/Tabs/Switcher/FloatingTabSwitcher.swift b/ora/Features/Tabs/Switcher/FloatingTabSwitcher.swift index fc614b68..3106d2dc 100644 --- a/ora/Features/Tabs/Switcher/FloatingTabSwitcher.swift +++ b/ora/Features/Tabs/Switcher/FloatingTabSwitcher.swift @@ -15,6 +15,8 @@ struct FloatingTabSwitcher: View { @FocusState private var focusedTab: Tab.ID? @State private var tabSnapshots: [Tab: TabSnapshot] = [:] @State private var isLoadingSnapshots = false + @State private var mouseHasMoved = false + @State private var mouseMonitor: Any? // MARK: - Constants @@ -40,6 +42,10 @@ struct FloatingTabSwitcher: View { let to = recentTabs.count == 1 ? 0 : 1 focusedTab = recentTabs[to].id } + startMouseMonitor() + } + .onDisappear { + stopMouseMonitor() } .onChange(of: appState.isFloatingTabSwitchVisible) { _, isVisible in if isVisible { @@ -48,6 +54,9 @@ struct FloatingTabSwitcher: View { let to = recentTabs.count == 1 ? 0 : 1 focusedTab = recentTabs[to].id } + startMouseMonitor() + } else { + stopMouseMonitor() } } .onChange(of: keyModifierListener.modifierFlags) { _, newFlags in @@ -128,7 +137,7 @@ struct FloatingTabSwitcher: View { .frame(width: Constants.previewWidth, alignment: .leading) .padding(.horizontal, 4) .onHover { isHovered in - if isHovered { + if isHovered, mouseHasMoved { focusedTab = tab.id } } @@ -327,6 +336,25 @@ struct FloatingTabSwitcher: View { BrowserSnapshotConfiguration(rect: nil, afterScreenUpdates: false) } + private func startMouseMonitor() { + mouseHasMoved = false + mouseMonitor = NSEvent.addLocalMonitorForEvents(matching: .mouseMoved) { event in + mouseHasMoved = true + if let monitor = mouseMonitor { + NSEvent.removeMonitor(monitor) + mouseMonitor = nil + } + return event + } + } + + private func stopMouseMonitor() { + if let monitor = mouseMonitor { + NSEvent.removeMonitor(monitor) + mouseMonitor = nil + } + } + private func closeFloatingTabSwitch() { appState.isFloatingTabSwitchVisible = false } From bb5f05df1e47f3440c7f37f8074d3f421cbee726 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 21:07:08 +0300 Subject: [PATCH 13/21] feat: add "Continue Anyway" option for SSL certificate errors - Add SSL bypass support in BrowserPage with per-host tracking - Handle URLAuthenticationChallenge to accept bypassed hosts - Add onContinueAnyway callback to StatusPageView for security errors - Add labelColorOverride to OraButton for muted ghost button styling - Wire continueToInsecureSite through BrowserWebContentView Co-Authored-By: Claude Opus 4.6 (1M context) --- ora/Core/BrowserEngine/BrowserPage.swift | 20 +++++++++++++++++++ .../Browser/Views/BrowserWebContentView.swift | 5 ++++- .../Browser/Views/StatusPageView.swift | 11 ++++++++++ ora/Shared/Components/Buttons/OraButton.swift | 2 ++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/ora/Core/BrowserEngine/BrowserPage.swift b/ora/Core/BrowserEngine/BrowserPage.swift index 0f201338..8e90201e 100644 --- a/ora/Core/BrowserEngine/BrowserPage.swift +++ b/ora/Core/BrowserEngine/BrowserPage.swift @@ -10,6 +10,7 @@ final class BrowserPage: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptM private var originalURL: URL? private(set) var lastCommittedURL: URL? private(set) var isDownloadNavigation = false + private(set) var sslBypassedHosts: Set = [] init( profile: BrowserEngineProfile, @@ -159,6 +160,10 @@ final class BrowserPage: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptM webView.removeFromSuperview() } + func bypassSSL(for host: String) { + sslBypassedHosts.insert(host) + } + private func emitNavigationEvent( phase: BrowserNavigationPhase, url: URL?, @@ -306,6 +311,21 @@ final class BrowserPage: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptM originalURL = nil } + func webView( + _ webView: WKWebView, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let serverTrust = challenge.protectionSpace.serverTrust, + sslBypassedHosts.contains(challenge.protectionSpace.host) + { + completionHandler(.useCredential, URLCredential(trust: serverTrust)) + } else { + completionHandler(.performDefaultHandling, nil) + } + } + @available(macOS 11.3, *) func webView( _ webView: WKWebView, diff --git a/ora/Features/Browser/Views/BrowserWebContentView.swift b/ora/Features/Browser/Views/BrowserWebContentView.swift index c2fdcbcd..77e99e99 100644 --- a/ora/Features/Browser/Views/BrowserWebContentView.swift +++ b/ora/Features/Browser/Views/BrowserWebContentView.swift @@ -35,7 +35,10 @@ struct BrowserWebContentView: View { ? { tab.goBack() tab.clearNavigationError() - } : nil + } : nil, + onContinueAnyway: { + tab.continueToInsecureSite() + } ) .id(tab.id) } else if let page = tab.browserPage { diff --git a/ora/Features/Browser/Views/StatusPageView.swift b/ora/Features/Browser/Views/StatusPageView.swift index 111c3348..5b6c2b09 100644 --- a/ora/Features/Browser/Views/StatusPageView.swift +++ b/ora/Features/Browser/Views/StatusPageView.swift @@ -7,6 +7,7 @@ struct StatusPageView: View { let failedURL: URL? let onRetry: () -> Void let onGoBack: (() -> Void)? + var onContinueAnyway: (() -> Void)? @Environment(\.theme) var theme var body: some View { @@ -59,6 +60,16 @@ struct StatusPageView: View { } .padding(.top, 8) + if errorType == .security, let continueAnyway = onContinueAnyway { + OraButton( + label: "Continue Anyway", + variant: .ghost, + size: .sm, + labelColorOverride: theme.mutedForeground, + action: continueAnyway + ) + } + Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/ora/Shared/Components/Buttons/OraButton.swift b/ora/Shared/Components/Buttons/OraButton.swift index c1fcbcc3..5160c157 100644 --- a/ora/Shared/Components/Buttons/OraButton.swift +++ b/ora/Shared/Components/Buttons/OraButton.swift @@ -23,6 +23,7 @@ struct OraButton: View { var keyboardShortcut: String? var leadingIcon: String? var trailingIcon: String? + var labelColorOverride: Color? let action: () -> Void @Environment(\.theme) private var theme @@ -96,6 +97,7 @@ struct OraButton: View { private var labelColor: Color { guard !isDisabled else { return theme.disabledForeground } + if let override = labelColorOverride { return override } switch variant { case .default, .destructive: return .white From 163a7400b5a4fee5935ab1471992deb84f521d25 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 21:22:57 +0300 Subject: [PATCH 14/21] fix: improve AI query detection heuristics in launcher suggestions Replace broken regex patterns (checked via .contains instead of regex evaluation) with proper heuristic layers: question prefixes, question marks, imperative phrases, action keywords, and a word count threshold. Add negative signals to skip single words and valid URLs. --- .../Launcher/State/LauncherViewModel.swift | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/ora/Features/Launcher/State/LauncherViewModel.swift b/ora/Features/Launcher/State/LauncherViewModel.swift index b5708237..cdc6ac27 100644 --- a/ora/Features/Launcher/State/LauncherViewModel.swift +++ b/ora/Features/Launcher/State/LauncherViewModel.swift @@ -275,17 +275,54 @@ class LauncherViewModel: ObservableObject { } private func isAISuitableQuery(_ query: String) -> Bool { - let lowercased = query.lowercased() - - let aiKeywords = [ - #"^(who|when|where|what|how|why)\b.*\?$"#, - #"^\d{4}"#, - "summarize", "rewrite", "explain", "code", "how to", "generate", - "idea", "opinion", "feedback", "story", "joke", "email", "draft", - "translate", "compare", "alternatives", "improve", "fix", "suggest" + let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines) + let lowercased = trimmed.lowercased() + let words = lowercased.split(separator: " ") + + // Negative signals: single words and URLs are not AI queries + if words.count <= 1 { return false } + if isValidURL(trimmed) { return false } + + // Starts with a question word + let questionPrefixes = [ + "who ", "what ", "where ", "when ", "how ", "why ", "which ", + "is ", "are ", "can ", "does ", "do ", "should ", "would ", + "could ", "will ", "was ", "were ", "has ", "have " ] + if questionPrefixes.contains(where: { lowercased.hasPrefix($0) }) { + return true + } + + // Ends with a question mark + if trimmed.hasSuffix("?") { + return true + } + + // Imperative / command phrases + let imperativePhrases = [ + "write me", "help me", "create a", "give me", "list of", + "make a", "tell me", "show me", "find me", "build a", + "design a", "plan a", "write a", "make me", "help with" + ] + if imperativePhrases.contains(where: { lowercased.contains($0) }) { + return true + } + + // Action keywords + let actionKeywords = [ + "summarize", "rewrite", "explain", "generate", "how to", + "translate", "compare", "alternatives", "improve", "suggest", + "recommend", "analyze", "convert", "calculate", "define", + "describe", "simplify", "debug", "optimize", "refactor", + "review", "draft", "code", "idea", "opinion", "story", + "joke", "email" + ] + if actionKeywords.contains(where: { lowercased.contains($0) }) { + return true + } - for keyword in aiKeywords where lowercased.contains(keyword) { + // Natural language heuristic: 4+ words likely conversational + if words.count >= 4 { return true } From 88e2c494299e57932a1a12cdddb4c76d29331f1e Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 21:46:21 +0300 Subject: [PATCH 15/21] fix: hide duplicate URL in launcher URL suggestions Skip showing the em-dash URL suffix when the suggestion title is already the URL itself. History items with distinct page titles still display their URL. --- ora/Features/Launcher/State/LauncherViewModel.swift | 2 +- .../Launcher/Suggestions/LauncherSuggestionItem.swift | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ora/Features/Launcher/State/LauncherViewModel.swift b/ora/Features/Launcher/State/LauncherViewModel.swift index cdc6ac27..1e6c3dda 100644 --- a/ora/Features/Launcher/State/LauncherViewModel.swift +++ b/ora/Features/Launcher/State/LauncherViewModel.swift @@ -204,7 +204,7 @@ class LauncherViewModel: ObservableObject { suggestions.append( LauncherSuggestion( type: .suggestedQuery, - title: "\(text) - Search with \(engineName)", + title: "\(text) - \(engineName)", action: { [weak self] in self?.onSubmit?(nil) } ) ) diff --git a/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift b/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift index 89b25c69..4220d117 100644 --- a/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift +++ b/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift @@ -14,7 +14,13 @@ struct LauncherSuggestionItem: View { } private var shouldShowURL: Bool { - suggestion.url != nil && !isAIChat && suggestion.type != .suggestedQuery && suggestion.type != .openedTab + guard let url = suggestion.url else { return false } + if isAIChat || suggestion.type == .suggestedQuery || suggestion.type == .openedTab { return false } + let urlString = url.absoluteString + if suggestion.title == urlString || urlString.hasSuffix("://\(suggestion.title)") || urlString + .hasSuffix("://\(suggestion.title)/") + { return false } + return true } private var isFocusedOrHovered: Bool { From 876d08ec31fc43cd6087211ff18663a6b0e28fb3 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 22:44:48 +0300 Subject: [PATCH 16/21] refactor: use ConditionallyConcentricRectangle in launcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace RoundedRectangle and .cornerRadius with ConditionallyConcentricRectangle and .clipShape for concentric corner support on macOS 26+. Bump radii slightly (container 16→20, badges 6→8, suggestion rows 8→10). --- ora/Features/Launcher/Main/LauncherMain.swift | 10 +++++----- .../Launcher/Suggestions/LauncherSuggestionItem.swift | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ora/Features/Launcher/Main/LauncherMain.swift b/ora/Features/Launcher/Main/LauncherMain.swift index b2a32224..b62a23d1 100644 --- a/ora/Features/Launcher/Main/LauncherMain.swift +++ b/ora/Features/Launcher/Main/LauncherMain.swift @@ -76,15 +76,15 @@ struct LauncherMain: View { .frame(minWidth: 320, maxWidth: 814, alignment: .leading) .background(theme.launcherMainBackground) .background(BlurEffectView(material: .popover, blendingMode: .withinWindow)) - .cornerRadius(16) + .clipShape(ConditionallyConcentricRectangle(cornerRadius: 20, style: .continuous)) .overlay( - RoundedRectangle(cornerRadius: 16, style: .continuous) - .inset(by: 0.25) + ConditionallyConcentricRectangle(cornerRadius: 20, style: .continuous) .stroke( Color(match?.color ?? theme.foreground) - .opacity(0.15), - lineWidth: 0.5 + .opacity(0.05), + lineWidth: 1 ) + .padding(0.25) ) .shadow( color: Color.black.opacity(0.1), diff --git a/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift b/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift index 4220d117..7bd7ea32 100644 --- a/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift +++ b/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift @@ -76,7 +76,7 @@ struct LauncherSuggestionItem: View { .padding(.horizontal, 8) .padding(.vertical, 4) .background(theme.foreground.opacity(0.07)) - .cornerRadius(6) + .clipShape(ConditionallyConcentricRectangle(cornerRadius: 8, style: .continuous)) } else if suggestion.type == .openedTab { HStack(alignment: .center, spacing: 8) { Text("Switch to tab ") @@ -90,7 +90,7 @@ struct LauncherSuggestionItem: View { .frame(width: 12, height: 12) .padding(6) .background( - RoundedRectangle(cornerRadius: 6, style: .continuous) + ConditionallyConcentricRectangle(cornerRadius: 8, style: .continuous) .fill( isFocusedOrHovered ? theme.foreground : theme.foreground.opacity(0.07) @@ -100,7 +100,7 @@ struct LauncherSuggestionItem: View { isFocusedOrHovered ? theme.background : .secondary ) } - .cornerRadius(6) + .clipShape(ConditionallyConcentricRectangle(cornerRadius: 8, style: .continuous)) } } @@ -130,7 +130,7 @@ struct LauncherSuggestionItem: View { .padding(.vertical, 10) .frame(width: 798, alignment: .leading) .background(backgroundColor) - .cornerRadius(8) + .clipShape(ConditionallyConcentricRectangle(cornerRadius: 10, style: .continuous)) .onTapGesture { suggestion.action() appState.showLauncher = false From bcb55a33cdbc44c8de5105472e7a7f4fd404c1df Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 22:44:53 +0300 Subject: [PATCH 17/21] fix: remove divider between launcher input and suggestions --- .../Launcher/Suggestions/LauncherSuggestionsView.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ora/Features/Launcher/Suggestions/LauncherSuggestionsView.swift b/ora/Features/Launcher/Suggestions/LauncherSuggestionsView.swift index 97d2e464..2057042e 100644 --- a/ora/Features/Launcher/Suggestions/LauncherSuggestionsView.swift +++ b/ora/Features/Launcher/Suggestions/LauncherSuggestionsView.swift @@ -17,11 +17,5 @@ struct LauncherSuggestionsView: View { } .frame(maxWidth: .infinity) .padding(.top, 4) - .overlay( - Rectangle() - .frame(height: 1) - .foregroundColor(theme.border.opacity(0.5)), - alignment: .top - ) } } From 36cde88ecd0ddb58f29a47657f790a981300f1e8 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 22:44:58 +0300 Subject: [PATCH 18/21] fix: darken launcher background in dark mode --- ora/Core/Constants/Theme.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ora/Core/Constants/Theme.swift b/ora/Core/Constants/Theme.swift index a0e86d80..2af22482 100644 --- a/ora/Core/Constants/Theme.swift +++ b/ora/Core/Constants/Theme.swift @@ -66,7 +66,7 @@ struct Theme: Equatable { } var launcherMainBackground: Color { - colorScheme == .dark ? Color(.windowBackgroundColor).opacity(0.7) : .white.opacity(0.8) + colorScheme == .dark ? self.popoverBackground.opacity(0.75) : .white.opacity(0.8) } var placeholder: Color { From 728c5e368edfcdaf6bc24a757f55e778ec3e4838 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 22:45:01 +0300 Subject: [PATCH 19/21] fix: reduce home view watermark opacity --- ora/Features/Browser/Views/HomeView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ora/Features/Browser/Views/HomeView.swift b/ora/Features/Browser/Views/HomeView.swift index 0177192a..4c6eae1d 100644 --- a/ora/Features/Browser/Views/HomeView.swift +++ b/ora/Features/Browser/Views/HomeView.swift @@ -10,7 +10,7 @@ struct HomeView: View { .ignoresSafeArea(.all) .frame(maxWidth: .infinity, maxHeight: .infinity) .contentShape(Rectangle()) - .background(theme.background.opacity(0.65)) + .background(theme.background.opacity(0.85)) .background( BlurEffectView(material: .underWindowBackground, blendingMode: .behindWindow) ) @@ -36,11 +36,11 @@ struct HomeView: View { .resizable() .renderingMode(.template) .frame(width: 50, height: 50) - .foregroundColor(theme.foreground.opacity(0.3)) + .foregroundColor(theme.foreground.opacity(0.2)) Text("Less noise, more browsing.") .font(.system(size: 16, weight: .semibold)) - .foregroundColor(theme.foreground.opacity(0.3)) + .foregroundColor(theme.foreground.opacity(0.2)) } .offset(x: -10, y: 120) .zIndex(2) From 591260d85312ee89daeae9967f3956540effb90e Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 22:45:12 +0300 Subject: [PATCH 20/21] fix: use ConditionallyConcentricRectangle in download row, adjust icon size --- ora/Features/Downloads/Views/DownloadHistoryRow.swift | 2 +- ora/Shared/Components/Icons/OraIcon.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ora/Features/Downloads/Views/DownloadHistoryRow.swift b/ora/Features/Downloads/Views/DownloadHistoryRow.swift index 594ee5f8..c56aecd2 100644 --- a/ora/Features/Downloads/Views/DownloadHistoryRow.swift +++ b/ora/Features/Downloads/Views/DownloadHistoryRow.swift @@ -63,7 +63,7 @@ struct DownloadHistoryRow: View { .padding(.horizontal, 6) .padding(.vertical, 6) .background( - RoundedRectangle(cornerRadius: 6) + ConditionallyConcentricRectangle(cornerRadius: 12) .fill(isHovered ? theme.mutedBackground.opacity(0.5) : .clear) ) .contentShape(Rectangle()) diff --git a/ora/Shared/Components/Icons/OraIcon.swift b/ora/Shared/Components/Icons/OraIcon.swift index 9274a0be..f5d87f6b 100644 --- a/ora/Shared/Components/Icons/OraIcon.swift +++ b/ora/Shared/Components/Icons/OraIcon.swift @@ -9,7 +9,7 @@ enum OraIconSize { var dimension: CGFloat { switch self { case .xs: 10 - case .sm: 14 + case .sm: 12 case .md: 16 case .lg: 20 case .xl: 24 From 0f576e6eaa8310f5a4c0f9e50d9a9665c2087607 Mon Sep 17 00:00:00 2001 From: yonaries Date: Sun, 15 Mar 2026 22:50:31 +0300 Subject: [PATCH 21/21] style: increase corner radius on suggestion item --- ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift b/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift index 7bd7ea32..d2edc5fb 100644 --- a/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift +++ b/ora/Features/Launcher/Suggestions/LauncherSuggestionItem.swift @@ -130,7 +130,7 @@ struct LauncherSuggestionItem: View { .padding(.vertical, 10) .frame(width: 798, alignment: .leading) .background(backgroundColor) - .clipShape(ConditionallyConcentricRectangle(cornerRadius: 10, style: .continuous)) + .clipShape(ConditionallyConcentricRectangle(cornerRadius: 12, style: .continuous)) .onTapGesture { suggestion.action() appState.showLauncher = false