From 83c745b5f03566705be1edc40ba40ecea0d53993 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 16:05:51 +0000 Subject: [PATCH 1/5] fix(desktop-ui): palette fuzzy search, label highlight, and shadow Co-authored-by: Tyler Dane --- .../CompassData/NativeCalendarOverlays.swift | 31 ++-- .../CommandPaletteSearchLogic.swift | 144 ++++++++++++++++++ .../CommandPaletteSearchLogicTests.swift | 25 +++ .../CommandPaletteHighlightedLabel.swift | 27 ++++ .../CommandPaletteOverlay.swift | 42 +++-- .../CompassUI/ThemeTokenColor+SwiftUI.swift | 2 + 6 files changed, 251 insertions(+), 20 deletions(-) create mode 100644 apps/calendar-macos/CompassKit/Sources/CompassKit/CommandPalette/CommandPaletteSearchLogic.swift create mode 100644 apps/calendar-macos/CompassKitTests/CommandPaletteSearchLogicTests.swift create mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteHighlightedLabel.swift diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift index fe419afbd..07d4d7fb0 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift @@ -344,17 +344,26 @@ public final class NoOpShortcutsCatalogPresenter: ShortcutsCatalogPresenting { enum CommandPaletteSearch { static func filter(sections: [CommandPaletteSection], query: String) -> [CommandPaletteSection] { - let needle = query.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - guard !needle.isEmpty else { return sections } - return sections.compactMap { section in - if section.id == "go-to-date" { return section } - let items = section.items.filter { item in - item.label.lowercased().contains(needle) - || item.keywords.contains { $0.lowercased().contains(needle) } - || (item.shortcut?.lowercased().contains(needle) ?? false) - } - guard !items.isEmpty else { return nil } - return CommandPaletteSection(id: section.id, heading: section.heading, items: items) + let mapped = sections.map { section in + CommandPaletteSearchLogic.Section( + id: section.id, + heading: section.heading, + items: section.items.map { + CommandPaletteSearchLogic.CommandPaletteSearchItem( + id: $0.id, + label: $0.label, + keywords: $0.keywords + ($0.shortcut.map { [$0] } ?? [])) + }) + } + let filtered = CommandPaletteSearchLogic.filter(sections: mapped, query: query) + let byId = Dictionary(uniqueKeysWithValues: sections.flatMap { section in + section.items.map { ($0.id, $0) } + }) + return filtered.map { section in + CommandPaletteSection( + id: section.id, + heading: section.heading, + items: section.items.compactMap { byId[$0.id] }) } } } diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/CommandPalette/CommandPaletteSearchLogic.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/CommandPalette/CommandPaletteSearchLogic.swift new file mode 100644 index 000000000..a4c7223da --- /dev/null +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/CommandPalette/CommandPaletteSearchLogic.swift @@ -0,0 +1,144 @@ +import Foundation + +/// Fuzzy command palette filter and label highlight ranges. Mirrors web `command-palette.search.ts`. +public enum CommandPaletteSearchLogic { + private static let minSubsequenceTokenLength = 3 + + public struct ScoredItem: Hashable, Sendable { + public let item: CommandPaletteSearchItem + public let score: Int + } + + public struct CommandPaletteSearchItem: Hashable, Sendable { + public let id: String + public let label: String + public let keywords: [String] + + public init(id: String, label: String, keywords: [String] = []) { + self.id = id + self.label = label + self.keywords = keywords + } + } + + public struct Section: Hashable, Sendable { + public let id: String + public let heading: String + public let items: [CommandPaletteSearchItem] + + public init(id: String, heading: String, items: [CommandPaletteSearchItem]) { + self.id = id + self.heading = heading + self.items = items + } + } + + public static func filter(sections: [Section], query: String) -> [Section] { + let tokens = tokenizeQuery(query) + if tokens.isEmpty { return sections } + + return sections.compactMap { section in + if section.id == "go-to-date" { return section } + let scored = section.items.compactMap { item -> ScoredItem? in + let score = scoreAgainstTokens(item: item, tokens: tokens) + guard score > 0 else { return nil } + return ScoredItem(item: item, score: score) + } + .sorted { $0.score > $1.score } + guard !scored.isEmpty else { return nil } + return Section( + id: section.id, + heading: section.heading, + items: scored.map(\.item)) + } + } + + public static func labelMatchRanges(label: String, query: String) -> [Range] { + let tokens = tokenizeQuery(query) + if tokens.isEmpty { return [] } + + let lowerLabel = label.lowercased() + var ranges: [(Int, Int)] = [] + for token in tokens { + if let index = lowerLabel.range(of: token)?.lowerBound { + let start = lowerLabel.distance(from: lowerLabel.startIndex, to: index) + ranges.append((start, start + token.count)) + } + } + if ranges.isEmpty { return [] } + + ranges.sort { $0.0 < $1.0 } + var merged: [(Int, Int)] = [ranges[0]] + for (start, end) in ranges.dropFirst() { + var last = merged[merged.count - 1] + if start <= last.1 { + last.1 = max(last.1, end) + merged[merged.count - 1] = last + } else { + merged.append((start, end)) + } + } + return merged.map { Range($0.0..<$0.1) } + } + + public static func scoreCommandItem(item: CommandPaletteSearchItem, query: String) -> Int { + let tokens = tokenizeQuery(query) + if tokens.isEmpty { return 0 } + return scoreAgainstTokens(item: item, tokens: tokens) + } + + private static func tokenizeQuery(_ query: String) -> [String] { + query + .trimmingCharacters(in: .whitespacesAndNewlines) + .lowercased() + .split(whereSeparator: { $0.isWhitespace }) + .map { normalizeSearchToken(String($0)) } + .filter { !$0.isEmpty } + } + + private static func normalizeSearchToken(_ token: String) -> String { + var t = token.trimmingCharacters(in: CharacterSet(charactersIn: "'\"")) + while t.hasPrefix("'") || t.hasPrefix("\"") { t.removeFirst() } + while t.hasSuffix("'") || t.hasSuffix("\"") { t.removeLast() } + return t.replacingOccurrences(of: #"[\.\!?]+$"#, with: "", options: .regularExpression) + } + + private static func scoreAgainstTokens(item: CommandPaletteSearchItem, tokens: [String]) -> Int { + let label = item.label.lowercased() + let keywords = item.keywords.map { $0.lowercased() } + var total = 0 + for token in tokens { + let labelTier = scoreToken(haystack: label, token: token) + let keywordTier = keywords.map { scoreToken(haystack: $0, token: token) }.max() ?? 0 + if labelTier == 0, keywordTier == 0 { return 0 } + let bestTier = max(labelTier, keywordTier) + total += bestTier * 2 + (labelTier == bestTier ? 1 : 0) + } + return total + } + + private static func scoreToken(haystack: String, token: String) -> Int { + if haystack == token { return 5 } + if haystack.hasPrefix(token) { return 4 } + if hasWordStartingWith(haystack: haystack, token: token) { return 3 } + if haystack.contains(token) { return 2 } + if token.count >= minSubsequenceTokenLength, isSubsequence(haystack: haystack, token: token) { + return 1 + } + return 0 + } + + private static func hasWordStartingWith(haystack: String, token: String) -> Bool { + haystack.split(whereSeparator: { $0.isWhitespace || $0 == "-" }) + .contains { $0.lowercased().hasPrefix(token) } + } + + private static func isSubsequence(haystack: String, token: String) -> Bool { + var position = haystack.startIndex + for char in token { + guard let idx = haystack[position...].firstIndex(of: char) else { return false } + position = haystack.index(after: idx) + } + return true + } +} diff --git a/apps/calendar-macos/CompassKitTests/CommandPaletteSearchLogicTests.swift b/apps/calendar-macos/CompassKitTests/CommandPaletteSearchLogicTests.swift new file mode 100644 index 000000000..4e7a72096 --- /dev/null +++ b/apps/calendar-macos/CompassKitTests/CommandPaletteSearchLogicTests.swift @@ -0,0 +1,25 @@ +import CompassKit +import XCTest + +final class CommandPaletteSearchLogicTests: XCTestCase { + func testScoreRequiresEveryToken() { + let item = CommandPaletteSearchLogic.CommandPaletteSearchItem( + id: "today", + label: "Go to Today", + keywords: ["now"]) + XCTAssertGreaterThan(CommandPaletteSearchLogic.scoreCommandItem(item: item, query: "go today"), 0) + XCTAssertEqual(CommandPaletteSearchLogic.scoreCommandItem(item: item, query: "go mars"), 0) + } + + func testLabelMatchRangesMergeAdjacent() { + let ranges = CommandPaletteSearchLogic.labelMatchRanges(label: "Go to Day", query: "go day") + XCTAssertEqual(ranges.map { ($0.lowerBound, $0.upperBound) }, [(0, 2), (6, 9)]) + } + + func testKeywordOnlyMatchDoesNotHighlightLabel() { + let ranges = CommandPaletteSearchLogic.labelMatchRanges( + label: "Play Block Party", + query: "game") + XCTAssertTrue(ranges.isEmpty) + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteHighlightedLabel.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteHighlightedLabel.swift new file mode 100644 index 000000000..52d713fd2 --- /dev/null +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteHighlightedLabel.swift @@ -0,0 +1,27 @@ +import CompassKit +import SwiftUI + +struct CommandPaletteHighlightedLabel: View { + @Environment(\.nativeWebTheme) private var theme + let label: String + let query: String + + var body: some View { + Text(attributedLabel) + .font(.rubik(14)) + } + + private var attributedLabel: AttributedString { + var result = AttributedString(label) + result.foregroundColor = theme.textColor + let ranges = CommandPaletteSearchLogic.labelMatchRanges(label: label, query: query) + for range in ranges { + guard let lower = Range(range, in: label) else { continue } + if let attrRange = Range(lower, in: result) { + result[attrRange].font = .rubik(14, weight: .semibold) + result[attrRange].foregroundColor = theme.accentColor + } + } + return result + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift index 9668c3bae..d5d249fdc 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift @@ -1,4 +1,5 @@ import CompassData +import CompassKit import SwiftUI public struct CommandPaletteOverlay: View { @@ -19,6 +20,7 @@ public struct CommandPaletteOverlay: View { if paletteStore.isOpen { let sections = model.filteredPaletteSections() let selectedId = model.selectedPaletteItemId + let query = paletteStore.query ZStack { theme.overlayBackdropColor .ignoresSafeArea() @@ -39,7 +41,7 @@ public struct CommandPaletteOverlay: View { ScrollView { VStack(alignment: .leading, spacing: 8) { if sections.isEmpty { - Text("No results for “\(paletteStore.query)”") + Text("No results for “\(query)”") .foregroundStyle(theme.textMutedColor) .padding(12) } @@ -50,7 +52,7 @@ public struct CommandPaletteOverlay: View { .foregroundStyle(theme.textMutedColor) } ForEach(section.items) { item in - paletteRow(item, selected: item.id == selectedId) + paletteRow(item, query: query, selected: item.id == selectedId) .id(item.id) } } @@ -86,6 +88,7 @@ public struct CommandPaletteOverlay: View { RoundedRectangle(cornerRadius: 12) .stroke(theme.borderColor, lineWidth: 1) ) + .shadow(color: theme.shadowDefaultColor, radius: 16, x: 0, y: 8) .accessibilityElement(children: .contain) .accessibilityIdentifier("compass-native-command-palette") } @@ -94,15 +97,13 @@ public struct CommandPaletteOverlay: View { } @ViewBuilder - private func paletteRow(_ item: CommandPaletteItem, selected: Bool) -> some View { + private func paletteRow(_ item: CommandPaletteItem, query: String, selected: Bool) -> some View { Button { model.runPaletteCommand(id: item.id) } label: { HStack { VStack(alignment: .leading, spacing: 2) { - Text(item.label) - .font(.rubik(14)) - .foregroundStyle(theme.textColor) + CommandPaletteHighlightedLabel(label: item.label, query: query) if let subtitle = item.subtitle { Text(subtitle) .font(.rubik(11)) @@ -111,19 +112,42 @@ public struct CommandPaletteOverlay: View { } Spacer() if let shortcut = item.shortcut { - Text(shortcut) - .font(.rubik(12)) + Text(paletteShortcutLabel(shortcut)) + .font(.rubik(11)) .foregroundStyle(theme.textMutedColor) } } .padding(.vertical, 6) .padding(.horizontal, 8) - .background(selected ? theme.accentColor.opacity(0.18) : theme.surfaceColor) + .background(selected ? theme.accentColor.opacity(0.12) : theme.surfaceColor) .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay { + if selected { + RoundedRectangle(cornerRadius: 8) + .stroke(theme.accentColor, lineWidth: 1) + } + } } .buttonStyle(.plain) .accessibilityValue(selected ? "Selected" : "Not selected") .accessibilityAddTraits(selected ? .isSelected : []) .accessibilityIdentifier("compass-native-palette-item-\(item.id)") } + + private func paletteShortcutLabel(_ shortcut: String) -> String { + let normalized = shortcut + .replacingOccurrences(of: "mod+", with: "Mod+") + .replacingOccurrences(of: "shift+", with: "Shift+") + .replacingOccurrences(of: "alt+", with: "Alt+") + let parts = normalized.split(separator: "+").map { part -> String in + let token = String(part) + switch token.lowercased() { + case "mod": return "Mod" + case "shift": return "Shift" + case "alt": return "Alt" + default: return token.count == 1 ? token.uppercased() : token + } + } + return KeycapGlyphs.display(parts) + } } diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/ThemeTokenColor+SwiftUI.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/ThemeTokenColor+SwiftUI.swift index c74fa3d6d..ed7268324 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/ThemeTokenColor+SwiftUI.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/ThemeTokenColor+SwiftUI.swift @@ -21,6 +21,7 @@ private protocol NativeThemePalette { var success: ThemeTokenColor { get } var warning: ThemeTokenColor { get } var overlayBackdrop: ThemeTokenColor { get } + var shadowDefault: ThemeTokenColor { get } } extension LightBeachPalette: NativeThemePalette {} @@ -70,6 +71,7 @@ public enum NativeWebTheme: String, Sendable, CaseIterable { public var successColor: Color { palette.success.swiftUIColor } public var warningColor: Color { palette.warning.swiftUIColor } public var overlayBackdropColor: Color { palette.overlayBackdrop.swiftUIColor } + public var shadowDefaultColor: Color { palette.shadowDefault.swiftUIColor } /// Web `OverlayPanel` backdrop (`bg-background/85`), used by modal panels. public var panelBackdropColor: Color { backgroundColor.opacity(0.85) } } From 553b9b2d17bf399f69bc49a810b899e5d313613b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 16:13:19 +0000 Subject: [PATCH 2/5] fix(desktop-ui): palette undo and redo rows from superseded PR Co-authored-by: Tyler Dane --- .../CommandPalette/CommandPaletteCatalog.swift | 10 ++++++++++ .../Sources/CompassData/NativeCalendarOverlays.swift | 4 ++++ .../CompassDataTests/CommandPaletteActionsTests.swift | 8 ++++++++ 3 files changed, 22 insertions(+) diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift b/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift index 48cbf72aa..e609ca474 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift @@ -170,6 +170,16 @@ public enum CommandPaletteCatalog { isTrialing: Bool ) -> [CommandPaletteSection] { var items: [CommandPaletteItem] = [ + CommandPaletteItem( + id: "undo", + label: "Undo", + shortcut: "mod+z", + keywords: ["revert", "back"]), + CommandPaletteItem( + id: "redo", + label: "Redo", + shortcut: "mod+shift+z", + keywords: ["repeat", "forward"]), CommandPaletteItem( id: "show-welcome-guide", label: "Show welcome guide", diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift index 07d4d7fb0..8f86f3c62 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift @@ -130,6 +130,10 @@ extension NativeCalendarRootModel { requestLogout() case "subscribe": handleShortcut(.otherSubscribe) + case "undo": + undoLastChange() + case "redo": + redoLastChange() default: if id.hasPrefix("event-") { let eventId = String(id.dropFirst("event-".count)) diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/CommandPaletteActionsTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/CommandPaletteActionsTests.swift index 0570c1673..a92377fc3 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/CommandPaletteActionsTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/CommandPaletteActionsTests.swift @@ -101,6 +101,14 @@ final class CommandPaletteActionsTests: XCTestCase { XCTAssertFalse(model.commandPaletteStore.recentCommandIds.contains("recent-today")) } + func testMoreSectionIncludesUndoAndRedoRows() async throws { + let model = try await makeModel() + model.commandPaletteStore.open() + let ids = Set(model.filteredPaletteSections().flatMap(\.items).map(\.id)) + XCTAssertTrue(ids.contains("undo")) + XCTAssertTrue(ids.contains("redo")) + } + func testAboutInvokesNativePresentation() async throws { let model = try await makeModel() var presented = false From fce0206b4ca81942d060db830106a696b2e47135 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 18:15:20 +0000 Subject: [PATCH 3/5] fix(desktop-ui): use Range literals in palette search merge Co-authored-by: Tyler Dane --- .../CompassKit/CommandPalette/CommandPaletteSearchLogic.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/CommandPalette/CommandPaletteSearchLogic.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/CommandPalette/CommandPaletteSearchLogic.swift index a4c7223da..6f135787e 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/CommandPalette/CommandPaletteSearchLogic.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/CommandPalette/CommandPaletteSearchLogic.swift @@ -78,7 +78,7 @@ public enum CommandPaletteSearchLogic { merged.append((start, end)) } } - return merged.map { Range($0.0..<$0.1) } + return merged.map { $0.0..<$0.1 } } public static func scoreCommandItem(item: CommandPaletteSearchItem, query: String) -> Int { From 36e895968f94e0e4eea9ca9bf3c11ad5f91011ea Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 18:47:53 +0000 Subject: [PATCH 4/5] fix(desktop-ui): map palette highlight ranges to String indices Co-authored-by: Tyler Dane --- .../CommandPalette/CommandPaletteHighlightedLabel.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteHighlightedLabel.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteHighlightedLabel.swift index 52d713fd2..bfee54ad1 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteHighlightedLabel.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteHighlightedLabel.swift @@ -16,7 +16,13 @@ struct CommandPaletteHighlightedLabel: View { result.foregroundColor = theme.textColor let ranges = CommandPaletteSearchLogic.labelMatchRanges(label: label, query: query) for range in ranges { - guard let lower = Range(range, in: label) else { continue } + guard range.lowerBound >= 0, + range.upperBound <= label.count, + range.lowerBound < range.upperBound + else { continue } + let start = label.index(label.startIndex, offsetBy: range.lowerBound) + let end = label.index(label.startIndex, offsetBy: range.upperBound) + let lower = start.. Date: Mon, 19 Oct 2026 19:21:09 +0000 Subject: [PATCH 5/5] test(desktop-ui): assert palette highlight ranges as Range values Co-authored-by: Tyler Dane --- .../CompassKitTests/CommandPaletteSearchLogicTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/calendar-macos/CompassKitTests/CommandPaletteSearchLogicTests.swift b/apps/calendar-macos/CompassKitTests/CommandPaletteSearchLogicTests.swift index 4e7a72096..f8d7cdbb1 100644 --- a/apps/calendar-macos/CompassKitTests/CommandPaletteSearchLogicTests.swift +++ b/apps/calendar-macos/CompassKitTests/CommandPaletteSearchLogicTests.swift @@ -13,7 +13,7 @@ final class CommandPaletteSearchLogicTests: XCTestCase { func testLabelMatchRangesMergeAdjacent() { let ranges = CommandPaletteSearchLogic.labelMatchRanges(label: "Go to Day", query: "go day") - XCTAssertEqual(ranges.map { ($0.lowerBound, $0.upperBound) }, [(0, 2), (6, 9)]) + XCTAssertEqual(ranges, [0..<2, 6..<9]) } func testKeywordOnlyMatchDoesNotHighlightLabel() {