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 )