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:
@@ -9,6 +9,7 @@ struct CollectionDocumentsOutline: View {
|
|||||||
@State private var viewModel: DocumentsViewModel
|
@State private var viewModel: DocumentsViewModel
|
||||||
let sortOption: SidebarSortOption
|
let sortOption: SidebarSortOption
|
||||||
let refreshToken: Int
|
let refreshToken: Int
|
||||||
|
let selectedDocumentID: String?
|
||||||
let onSelectDocument: (OutlineDocument) -> Void
|
let onSelectDocument: (OutlineDocument) -> Void
|
||||||
|
|
||||||
private var tree: [DocumentNode] {
|
private var tree: [DocumentNode] {
|
||||||
@@ -20,11 +21,13 @@ struct CollectionDocumentsOutline: View {
|
|||||||
collection: OutlineCollection,
|
collection: OutlineCollection,
|
||||||
sortOption: SidebarSortOption,
|
sortOption: SidebarSortOption,
|
||||||
refreshToken: Int,
|
refreshToken: Int,
|
||||||
|
selectedDocumentID: String?,
|
||||||
onSelectDocument: @escaping (OutlineDocument) -> Void
|
onSelectDocument: @escaping (OutlineDocument) -> Void
|
||||||
) {
|
) {
|
||||||
_viewModel = State(initialValue: DocumentsViewModel(apiClient: apiClient, collection: collection))
|
_viewModel = State(initialValue: DocumentsViewModel(apiClient: apiClient, collection: collection))
|
||||||
self.sortOption = sortOption
|
self.sortOption = sortOption
|
||||||
self.refreshToken = refreshToken
|
self.refreshToken = refreshToken
|
||||||
|
self.selectedDocumentID = selectedDocumentID
|
||||||
self.onSelectDocument = onSelectDocument
|
self.onSelectDocument = onSelectDocument
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +44,12 @@ struct CollectionDocumentsOutline: View {
|
|||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
} else {
|
} else {
|
||||||
ForEach(tree) { node in
|
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 {
|
private struct DocumentNodeRow: View {
|
||||||
let node: DocumentNode
|
let node: DocumentNode
|
||||||
let depth: Int
|
let depth: Int
|
||||||
|
let selectedDocumentID: String?
|
||||||
let onSelectDocument: (OutlineDocument) -> Void
|
let onSelectDocument: (OutlineDocument) -> Void
|
||||||
|
|
||||||
@State private var isExpanded = false
|
@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 {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
@@ -97,14 +117,33 @@ private struct DocumentNodeRow: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
|
.padding(.horizontal, 4)
|
||||||
.padding(.leading, CGFloat(depth) * 16)
|
.padding(.leading, CGFloat(depth) * 16)
|
||||||
|
.background(
|
||||||
|
isSelected ? Color.accentColor.opacity(0.15) : Color.clear,
|
||||||
|
in: RoundedRectangle(cornerRadius: 6)
|
||||||
|
)
|
||||||
|
|
||||||
if isExpanded {
|
if isExpanded {
|
||||||
ForEach(node.children) { child in
|
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
|
#endif
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ struct CollectionTreeRow: View {
|
|||||||
let collection: OutlineCollection
|
let collection: OutlineCollection
|
||||||
let isExpanded: Bool
|
let isExpanded: Bool
|
||||||
let isSelected: Bool
|
let isSelected: Bool
|
||||||
|
let selectedDocumentID: String?
|
||||||
let onToggle: () -> Void
|
let onToggle: () -> Void
|
||||||
let onSelectDocument: (OutlineDocument) -> Void
|
let onSelectDocument: (OutlineDocument) -> Void
|
||||||
let onSearchInCollection: (OutlineCollection) -> Void
|
let onSearchInCollection: (OutlineCollection) -> Void
|
||||||
@@ -52,6 +53,7 @@ struct CollectionTreeRow: View {
|
|||||||
collection: collection,
|
collection: collection,
|
||||||
sortOption: sortOption,
|
sortOption: sortOption,
|
||||||
refreshToken: documentsRefreshToken,
|
refreshToken: documentsRefreshToken,
|
||||||
|
selectedDocumentID: selectedDocumentID,
|
||||||
onSelectDocument: onSelectDocument
|
onSelectDocument: onSelectDocument
|
||||||
)
|
)
|
||||||
.padding(.leading, 18)
|
.padding(.leading, 18)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import OutlineKit
|
|||||||
struct CollectionsTreeView: View {
|
struct CollectionsTreeView: View {
|
||||||
@State private var viewModel: CollectionsViewModel
|
@State private var viewModel: CollectionsViewModel
|
||||||
@Binding private var selectedCollection: OutlineCollection?
|
@Binding private var selectedCollection: OutlineCollection?
|
||||||
|
let selectedDocumentID: String?
|
||||||
@State private var expandedCollectionIDs: Set<String> = []
|
@State private var expandedCollectionIDs: Set<String> = []
|
||||||
let onSelectDocument: (OutlineCollection, OutlineDocument) -> Void
|
let onSelectDocument: (OutlineCollection, OutlineDocument) -> Void
|
||||||
let onSearchInCollection: (OutlineCollection) -> Void
|
let onSearchInCollection: (OutlineCollection) -> Void
|
||||||
@@ -12,11 +13,13 @@ struct CollectionsTreeView: View {
|
|||||||
init(
|
init(
|
||||||
apiClient: OutlineAPIClient,
|
apiClient: OutlineAPIClient,
|
||||||
selectedCollection: Binding<OutlineCollection?>,
|
selectedCollection: Binding<OutlineCollection?>,
|
||||||
|
selectedDocumentID: String?,
|
||||||
onSelectDocument: @escaping (OutlineCollection, OutlineDocument) -> Void,
|
onSelectDocument: @escaping (OutlineCollection, OutlineDocument) -> Void,
|
||||||
onSearchInCollection: @escaping (OutlineCollection) -> Void
|
onSearchInCollection: @escaping (OutlineCollection) -> Void
|
||||||
) {
|
) {
|
||||||
_viewModel = State(initialValue: CollectionsViewModel(apiClient: apiClient))
|
_viewModel = State(initialValue: CollectionsViewModel(apiClient: apiClient))
|
||||||
_selectedCollection = selectedCollection
|
_selectedCollection = selectedCollection
|
||||||
|
self.selectedDocumentID = selectedDocumentID
|
||||||
self.onSelectDocument = onSelectDocument
|
self.onSelectDocument = onSelectDocument
|
||||||
self.onSearchInCollection = onSearchInCollection
|
self.onSearchInCollection = onSearchInCollection
|
||||||
}
|
}
|
||||||
@@ -50,6 +53,7 @@ struct CollectionsTreeView: View {
|
|||||||
collection: collection,
|
collection: collection,
|
||||||
isExpanded: expandedCollectionIDs.contains(collection.id),
|
isExpanded: expandedCollectionIDs.contains(collection.id),
|
||||||
isSelected: selectedCollection?.id == collection.id,
|
isSelected: selectedCollection?.id == collection.id,
|
||||||
|
selectedDocumentID: selectedDocumentID,
|
||||||
onToggle: { toggle(collection) },
|
onToggle: { toggle(collection) },
|
||||||
onSelectDocument: { document in onSelectDocument(collection, document) },
|
onSelectDocument: { document in onSelectDocument(collection, document) },
|
||||||
onSearchInCollection: onSearchInCollection,
|
onSearchInCollection: onSearchInCollection,
|
||||||
@@ -69,6 +73,13 @@ struct CollectionsTreeView: View {
|
|||||||
selectedCollection = first
|
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) {
|
private func toggle(_ collection: OutlineCollection) {
|
||||||
|
|||||||
@@ -14,20 +14,6 @@ struct ContentView_macOS: View {
|
|||||||
globalSearchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
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 {
|
var body: some View {
|
||||||
NavigationSplitView {
|
NavigationSplitView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
@@ -40,10 +26,13 @@ struct ContentView_macOS: View {
|
|||||||
} detail: {
|
} detail: {
|
||||||
detail
|
detail
|
||||||
}
|
}
|
||||||
// Was hardcoded to the team name and never updated — the actual macOS
|
// Empty rather than `windowTitle`: with a real value, the native title
|
||||||
// window title (separate from the leadingToolbarContent pill below)
|
// rendered in the same toolbar row as the custom `.navigation` pill
|
||||||
// stayed on the workspace name even with a collection/document open.
|
// below, visibly duplicating it. `.windowToolbarStyle(showsTitle:
|
||||||
.navigationTitle(windowTitle)
|
// 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 {
|
.toolbar {
|
||||||
// Per HIG: "sidebar-toggle/back controls appear at the far leading
|
// Per HIG: "sidebar-toggle/back controls appear at the far leading
|
||||||
// edge, followed by the view title" — this is that title, not
|
// edge, followed by the view title" — this is that title, not
|
||||||
@@ -112,6 +101,7 @@ struct ContentView_macOS: View {
|
|||||||
CollectionsTreeView(
|
CollectionsTreeView(
|
||||||
apiClient: apiClient,
|
apiClient: apiClient,
|
||||||
selectedCollection: $selectedCollection,
|
selectedCollection: $selectedCollection,
|
||||||
|
selectedDocumentID: documentPath.last?.id,
|
||||||
onSelectDocument: selectDocument,
|
onSelectDocument: selectDocument,
|
||||||
onSearchInCollection: searchInCollection
|
onSearchInCollection: searchInCollection
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user