fix(collections): show collection name in toolbar, highlight open doc

.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).
This commit is contained in:
2026-08-14 01:16:40 +01:00
parent 952a9620a9
commit a0643090b3
4 changed files with 62 additions and 20 deletions
@@ -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
@@ -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)
@@ -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<String> = []
let onSelectDocument: (OutlineCollection, OutlineDocument) -> Void
let onSearchInCollection: (OutlineCollection) -> Void
@@ -12,11 +13,13 @@ struct CollectionsTreeView: View {
init(
apiClient: OutlineAPIClient,
selectedCollection: Binding<OutlineCollection?>,
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) {
@@ -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
)