From bacd4f0db99aa88aafade9fcb2bdb41a9b5f8816 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Nov 2026 12:17:23 +0000 Subject: [PATCH 1/5] fix(native): match TS card gradients, palette ordering and remaining UI audit rows (batch 4, part 1) - Card fill gradients follow CSS premultiplied interpolation (Full effects sheen) - Card inset highlight fades within a corner radius, not 12% of the height - Remove the topbar sidebar trigger (second sidebar icon) - Background query line under the topbar, as AppLayout places it - Transactions: pencil and box outside the focusable row; bulk bar above search - Planned strip: 2/4/8 columns by breakpoint - Net worth By account as an inset list; asset card grid keeps a lone card in column one - Analysis workspace: saved cards in a two-column aside under the editor - Command palette orders every group by its best cmdk score Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018QJtaJ8FGv29hoMcZ1ASbV --- .../NativeCommandPaletteModel.swift | 47 +++++++--- .../AnalysisWorkspaceView.swift | 16 ++-- .../Components/NativeTopbar.swift | 21 ++--- .../Components/VisionCardLayers.swift | 62 ++++++++++++- .../Components/VisionResponsiveColumns.swift | 5 +- .../Sources/VisionNativeUI/NetWorthView.swift | 90 +++++++++++-------- .../VisionNativeUI/PlannedPaymentsView.swift | 16 +++- .../PortfolioHoldingsView.swift | 2 +- .../VisionNativeUI/ShellRootView.swift | 5 -- .../VisionNativeUI/TransactionsTable.swift | 58 +++++++----- .../CardGradientTests.swift | 37 ++++++++ 11 files changed, 251 insertions(+), 108 deletions(-) create mode 100644 apps/native/Tests/VisionNativeUITests/CardGradientTests.swift diff --git a/apps/native/Sources/VisionNativeCore/Navigation/NativeCommandPaletteModel.swift b/apps/native/Sources/VisionNativeCore/Navigation/NativeCommandPaletteModel.swift index 48a33ffa7..077e0f3e3 100644 --- a/apps/native/Sources/VisionNativeCore/Navigation/NativeCommandPaletteModel.swift +++ b/apps/native/Sources/VisionNativeCore/Navigation/NativeCommandPaletteModel.swift @@ -125,9 +125,25 @@ import Observation /// Dynamic answers/search hits are force-mounted, as in cmdk. Navigation/actions /// use cmdk's localized label + URL fuzzy score; groups/items keep stable ties. public func groups(strings: any Localizing) -> [NativeCommandPaletteGroup] { - var result: [NativeCommandPaletteGroup] = [] - func group(_ id: String, _ key: String, _ entries: [NativeCommandPaletteEntry]) { - if !entries.isEmpty { result.append(.init(id: id, title: strings.t(key), entries: entries)) } + // cmdk scores every item, force-mounted ones included, and orders the groups by their + // best item (ties keep mount order); force-mounted groups only stay visible at 0. So the + // ticker, answer, search and Payees groups take part in the ordering too, scored on the + // `value` strings the web items carry. + var filtered: [(group: NativeCommandPaletteGroup, score: Double, index: Int)] = [] + func group( + _ id: String, _ key: String, _ entries: [NativeCommandPaletteEntry], + value: (NativeCommandPaletteEntry) -> String + ) { + guard !entries.isEmpty else { return } + let scored = entries.enumerated().map { index, entry in + (entry: entry, score: query.isEmpty ? 1 : NativeCommandPaletteQueries.score(value(entry), query: query), + index: index) + }.sorted { $0.score == $1.score ? $0.index < $1.index : $0.score > $1.score } + filtered.append( + ( + .init(id: id, title: strings.t(key), entries: scored.map(\.entry)), scored[0].score, + filtered.count + )) } if let tickerSymbol, quote != nil || tickerLoading { let symbol = quote?.symbol ?? tickerSymbol @@ -140,7 +156,8 @@ import Observation action: .navigate( .init(.marketLookup, query: [.init(name: "symbol", value: symbol)])), quote: quote) - ]) + ] + ) { _ in "market \(symbol) \(self.quote?.name ?? "")" } } if let answer { group( @@ -150,7 +167,8 @@ import Observation id: "answer", title: answer.text, symbol: answer.kind == .fx ? "arrow.left.arrow.right" : "plus.forwardslash.minus", hint: strings.t("commandPalette.copyHint"), action: .copy(answer.text)) - ]) + ] + ) { _ in "result \(query)" } } if trimmedQuery.utf16.count >= 2 { group( @@ -162,7 +180,8 @@ import Observation symbol: "list.bullet.rectangle.portrait", hint: nil, action: .navigate( .init(.transactions, query: [.init(name: "search", value: trimmedQuery)]))) - ]) + ] + ) { _ in "tx-search \(query)" } } group( "recipients", "nav.recipients", @@ -176,7 +195,8 @@ import Observation .init(name: "recipient_id", value: String($0.id.rawValue)), .init(name: "filter_label", value: $0.name), ]))) - }) + } + ) { "recipient \($0.title) \(query)" } let pages = Self.pageGroups(adminMode: settings.adminMode).flatMap(\.pages) if trimmedQuery.isEmpty { group( @@ -184,17 +204,17 @@ import Observation recents.enumerated().compactMap { index, url in guard let page = pages.first(where: { $0.url == url }) else { return nil } return pageEntry(page, strings: strings, prefix: "recent-\(index)-") - }) + } + ) { "recent \($0.title) \(navigationURL($0.action))" } } - var filtered: [(group: NativeCommandPaletteGroup, score: Double, index: Int)] = [] - for (index, section) in Self.pageGroups(adminMode: settings.adminMode).enumerated() { + for section in Self.pageGroups(adminMode: settings.adminMode) { let entries = section.pages.map { pageEntry($0, strings: strings) } let ranked = ranked(entries) { $0.title + " " + navigationURL($0.action) } if !ranked.isEmpty { filtered.append( ( .init(id: section.key, title: strings.t(section.key), entries: ranked.map(\.entry)), - ranked[0].score, index + ranked[0].score, filtered.count )) } } @@ -221,12 +241,11 @@ import Observation ( .init( id: "actions", title: strings.t("commandPalette.actions"), entries: rankedActions.map(\.entry)), - rankedActions[0].score, filtered.count + 5 + rankedActions[0].score, filtered.count )) } if !query.isEmpty { filtered.sort { $0.score == $1.score ? $0.index < $1.index : $0.score > $1.score } } - result += filtered.map(\.group) - return result + return filtered.map(\.group) } private static func pageGroups(adminMode: Bool) -> [(key: String, pages: [NavItem])] { diff --git a/apps/native/Sources/VisionNativeUI/AnalysisWorkspaceView.swift b/apps/native/Sources/VisionNativeUI/AnalysisWorkspaceView.swift index dadf04e6b..877c2e92a 100644 --- a/apps/native/Sources/VisionNativeUI/AnalysisWorkspaceView.swift +++ b/apps/native/Sources/VisionNativeUI/AnalysisWorkspaceView.swift @@ -21,7 +21,7 @@ public struct AnalysisWorkspaceView: View { private var strings: any Localizing { shell.strings } public var body: some View { - GeometryReader { geometry in + GeometryReader { _ in ScrollViewReader { proxy in ScrollView { VStack(alignment: .leading, spacing: VisionPageLayout.spacing) { @@ -29,12 +29,10 @@ public struct AnalysisWorkspaceView: View { title: strings.t("analysis.title"), subtitle: strings.t("analysis.subtitle"), symbol: VisionPageSymbol.of("/analysis", fallback: "chart.bar")) workflow(proxy) - let wide = geometry.size.width >= 1140 - let layout = - wide - ? AnyLayout(HStackLayout(alignment: .top, spacing: 24)) - : AnyLayout(VStackLayout(alignment: .leading, spacing: 24)) - layout { + // The web page is one column: the editor and result cards, then + // `