From a0643090b3b7896c47a0bd5ced9fdfbd76c0423b Mon Sep 17 00:00:00 2001 From: psavarmattas Date: Fri, 14 Aug 2026 01:16:40 +0100 Subject: [PATCH] fix(collections): show collection name in toolbar, highlight open doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .navigationTitle set to a real value duplicated the custom toolbar pill (macOS renders both in the same row); the previous attempt to suppress just the native text via .windowToolbarStyle(showsTitle: false) also suppressed the custom .navigation item itself. Empty title sidesteps both. Also threads the open document's id down through the sidebar tree so its row highlights too, not just the containing collection — and auto-expands whichever collection contains it when the document was opened from outside the sidebar (detail pane, global search). --- .../CollectionDocumentsOutline.swift | 43 ++++++++++++++++++- .../Collections/CollectionTreeRow.swift | 2 + .../Collections/CollectionsTreeView.swift | 11 +++++ .../Collections/ContentView_macOS.swift | 26 ++++------- 4 files changed, 62 insertions(+), 20 deletions(-) diff --git a/Outpost/Features/Collections/CollectionDocumentsOutline.swift b/Outpost/Features/Collections/CollectionDocumentsOutline.swift index 96958b1..652cec7 100644 --- a/Outpost/Features/Collections/CollectionDocumentsOutline.swift +++ b/Outpost/Features/Collections/CollectionDocumentsOutline.swift @@ -9,6 +9,7 @@ struct CollectionDocumentsOutline: View { @State private var viewModel: DocumentsViewModel let sortOption: SidebarSortOption let refreshToken: Int + let selectedDocumentID: String? let onSelectDocument: (OutlineDocument) -> Void private var tree: [DocumentNode] { @@ -20,11 +21,13 @@ struct CollectionDocumentsOutline: View { collection: OutlineCollection, sortOption: SidebarSortOption, refreshToken: Int, + selectedDocumentID: String?, onSelectDocument: @escaping (OutlineDocument) -> Void ) { _viewModel = State(initialValue: DocumentsViewModel(apiClient: apiClient, collection: collection)) self.sortOption = sortOption self.refreshToken = refreshToken + self.selectedDocumentID = selectedDocumentID self.onSelectDocument = onSelectDocument } @@ -41,7 +44,12 @@ struct CollectionDocumentsOutline: View { .padding(.vertical, 6) } else { ForEach(tree) { node in - DocumentNodeRow(node: node, depth: 0, onSelectDocument: onSelectDocument) + DocumentNodeRow( + node: node, + depth: 0, + selectedDocumentID: selectedDocumentID, + onSelectDocument: onSelectDocument + ) } } } @@ -52,10 +60,22 @@ struct CollectionDocumentsOutline: View { private struct DocumentNodeRow: View { let node: DocumentNode let depth: Int + let selectedDocumentID: String? let onSelectDocument: (OutlineDocument) -> Void @State private var isExpanded = false + private var isSelected: Bool { + node.id == selectedDocumentID + } + + private func containsSelected(_ node: DocumentNode) -> Bool { + guard let selectedDocumentID else { return false } + return node.children.contains { + $0.id == selectedDocumentID || containsSelected($0) + } + } + var body: some View { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 6) { @@ -97,14 +117,33 @@ private struct DocumentNodeRow: View { .buttonStyle(.plain) } .padding(.vertical, 6) + .padding(.horizontal, 4) .padding(.leading, CGFloat(depth) * 16) + .background( + isSelected ? Color.accentColor.opacity(0.15) : Color.clear, + in: RoundedRectangle(cornerRadius: 6) + ) if isExpanded { ForEach(node.children) { child in - DocumentNodeRow(node: child, depth: depth + 1, onSelectDocument: onSelectDocument) + DocumentNodeRow( + node: child, + depth: depth + 1, + selectedDocumentID: selectedDocumentID, + onSelectDocument: onSelectDocument + ) } } } + // Reveals the selected document if it's nested under this node — + // both on first appearance and whenever the selection changes — but + // never auto-collapses on the way out, so manual expand/collapse + // elsewhere in the tree isn't fought. + .task(id: selectedDocumentID) { + if containsSelected(node) { + isExpanded = true + } + } } } #endif diff --git a/Outpost/Features/Collections/CollectionTreeRow.swift b/Outpost/Features/Collections/CollectionTreeRow.swift index bcfe9b8..46478cc 100644 --- a/Outpost/Features/Collections/CollectionTreeRow.swift +++ b/Outpost/Features/Collections/CollectionTreeRow.swift @@ -7,6 +7,7 @@ struct CollectionTreeRow: View { let collection: OutlineCollection let isExpanded: Bool let isSelected: Bool + let selectedDocumentID: String? let onToggle: () -> Void let onSelectDocument: (OutlineDocument) -> Void let onSearchInCollection: (OutlineCollection) -> Void @@ -52,6 +53,7 @@ struct CollectionTreeRow: View { collection: collection, sortOption: sortOption, refreshToken: documentsRefreshToken, + selectedDocumentID: selectedDocumentID, onSelectDocument: onSelectDocument ) .padding(.leading, 18) diff --git a/Outpost/Features/Collections/CollectionsTreeView.swift b/Outpost/Features/Collections/CollectionsTreeView.swift index 84403cb..2663378 100644 --- a/Outpost/Features/Collections/CollectionsTreeView.swift +++ b/Outpost/Features/Collections/CollectionsTreeView.swift @@ -5,6 +5,7 @@ import OutlineKit struct CollectionsTreeView: View { @State private var viewModel: CollectionsViewModel @Binding private var selectedCollection: OutlineCollection? + let selectedDocumentID: String? @State private var expandedCollectionIDs: Set = [] let onSelectDocument: (OutlineCollection, OutlineDocument) -> Void let onSearchInCollection: (OutlineCollection) -> Void @@ -12,11 +13,13 @@ struct CollectionsTreeView: View { init( apiClient: OutlineAPIClient, selectedCollection: Binding, + selectedDocumentID: String?, onSelectDocument: @escaping (OutlineCollection, OutlineDocument) -> Void, onSearchInCollection: @escaping (OutlineCollection) -> Void ) { _viewModel = State(initialValue: CollectionsViewModel(apiClient: apiClient)) _selectedCollection = selectedCollection + self.selectedDocumentID = selectedDocumentID self.onSelectDocument = onSelectDocument self.onSearchInCollection = onSearchInCollection } @@ -50,6 +53,7 @@ struct CollectionsTreeView: View { collection: collection, isExpanded: expandedCollectionIDs.contains(collection.id), isSelected: selectedCollection?.id == collection.id, + selectedDocumentID: selectedDocumentID, onToggle: { toggle(collection) }, onSelectDocument: { document in onSelectDocument(collection, document) }, onSearchInCollection: onSearchInCollection, @@ -69,6 +73,13 @@ struct CollectionsTreeView: View { selectedCollection = first } } + // A document opened from outside the sidebar (detail pane's list, + // global search) wouldn't otherwise expand its collection here, so + // the highlighted row would stay hidden. + .task(id: selectedDocumentID) { + guard selectedDocumentID != nil, let selectedCollection else { return } + expandedCollectionIDs.insert(selectedCollection.id) + } } private func toggle(_ collection: OutlineCollection) { diff --git a/Outpost/Features/Collections/ContentView_macOS.swift b/Outpost/Features/Collections/ContentView_macOS.swift index 74dfd25..ec5c636 100644 --- a/Outpost/Features/Collections/ContentView_macOS.swift +++ b/Outpost/Features/Collections/ContentView_macOS.swift @@ -14,20 +14,6 @@ struct ContentView_macOS: View { globalSearchQuery.trimmingCharacters(in: .whitespacesAndNewlines) } - /// Mirrors leadingToolbarContent's priority (search > document > collection - /// > workspace) so the actual window title and the custom pill agree. - private var windowTitle: String { - if !trimmedGlobalQuery.isEmpty { - return "Search" - } else if let currentDocument = documentPath.last { - return currentDocument.title.isEmpty ? "Untitled" : currentDocument.title - } else if let selectedCollection { - return selectedCollection.name - } else { - return session.teamName ?? "Outpost" - } - } - var body: some View { NavigationSplitView { VStack(spacing: 0) { @@ -40,10 +26,13 @@ struct ContentView_macOS: View { } detail: { detail } - // Was hardcoded to the team name and never updated — the actual macOS - // window title (separate from the leadingToolbarContent pill below) - // stayed on the workspace name even with a collection/document open. - .navigationTitle(windowTitle) + // Empty rather than `windowTitle`: with a real value, the native title + // rendered in the same toolbar row as the custom `.navigation` pill + // below, visibly duplicating it. `.windowToolbarStyle(showsTitle: + // false)` was tried to suppress just the native text, but it also + // suppressed the custom `.navigation` item itself — an empty title + // sidesteps both problems (Window-menu entry is blank as a result). + .navigationTitle("") .toolbar { // Per HIG: "sidebar-toggle/back controls appear at the far leading // edge, followed by the view title" — this is that title, not @@ -112,6 +101,7 @@ struct ContentView_macOS: View { CollectionsTreeView( apiClient: apiClient, selectedCollection: $selectedCollection, + selectedDocumentID: documentPath.last?.id, onSelectDocument: selectDocument, onSearchInCollection: searchInCollection )