Files
Outpost/Outpost/Features/Collections/ContentView_macOS.swift
T
Puranjay Savar Mattas a0643090b3 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).
2026-08-14 01:16:40 +01:00

168 lines
7.0 KiB
Swift

#if os(macOS)
import SwiftUI
import OutlineKit
struct ContentView_macOS: View {
@Environment(SessionStore.self) private var session
@State private var selectedCollection: OutlineCollection?
@State private var documentPath: [OutlineDocument] = []
@State private var globalSearchQuery = ""
@State private var contextualSearchQuery = ""
@FocusState private var isContextualSearchFocused: Bool
private var trimmedGlobalQuery: String {
globalSearchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
}
var body: some View {
NavigationSplitView {
VStack(spacing: 0) {
SidebarSearchField(text: $globalSearchQuery)
Divider()
sidebar
AccountFooter()
}
.navigationSplitViewColumnWidth(min: 220, ideal: 260)
} detail: {
detail
}
// 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
// centered. It doesn't collide with the back button because they're
// mutually exclusive: whenever a document's pushed (back button
// visible), this shows the document's own title instead of falling
// back to the workspace badge.
ToolbarItem(placement: .navigation) {
leadingToolbarContent
}
}
// Attached at this level (not inside the detail's NavigationStack) so
// it reliably renders in the shared window toolbar instead of a
// per-screen one that can get lost when nested more deeply.
.searchable(
text: $contextualSearchQuery,
placement: .toolbar,
prompt: selectedCollection.map { "Search \($0.name)" } ?? "Search"
)
.searchFocused($isContextualSearchFocused)
}
@ViewBuilder
private var leadingToolbarContent: some View {
Group {
if !trimmedGlobalQuery.isEmpty {
HStack(spacing: 6) {
Image(systemName: "magnifyingglass")
Text("Search")
}
} else if let currentDocument = documentPath.last, let selectedCollection {
// Back button (system-provided) → collection → document, so the
// hierarchy reads left to right instead of just the leaf title.
HStack(spacing: 6) {
CollectionRowView(collection: selectedCollection)
Image(systemName: "chevron.right")
.font(.caption2.weight(.semibold))
.foregroundStyle(.tertiary)
if let emoji = currentDocument.emoji {
Text(emoji)
} else {
Image(systemName: "doc.text")
}
Text(currentDocument.title.isEmpty ? "Untitled" : currentDocument.title)
.lineLimit(1)
}
} else if let selectedCollection {
CollectionRowView(collection: selectedCollection)
} else {
HStack(spacing: 8) {
AvatarBadge(avatarURL: session.teamAvatarURL, placeholderSystemImage: "text.book.closed.fill")
Text(session.teamName ?? "Outpost")
.lineLimit(1)
}
}
}
.font(.headline)
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(.fill.tertiary, in: Capsule())
}
@ViewBuilder
private var sidebar: some View {
if let apiClient = session.apiClient {
CollectionsTreeView(
apiClient: apiClient,
selectedCollection: $selectedCollection,
selectedDocumentID: documentPath.last?.id,
onSelectDocument: selectDocument,
onSearchInCollection: searchInCollection
)
} else {
ContentUnavailableView("Not Signed In", systemImage: "person.crop.circle.badge.exclamationmark")
}
}
private func searchInCollection(_ collection: OutlineCollection) {
globalSearchQuery = ""
selectedCollection = collection
Task { @MainActor in
isContextualSearchFocused = true
}
}
/// `documentPath = [document]` (replacing a same-size array in one step)
/// unreliably updates `NavigationStack` on macOS — the stack can silently
/// keep showing the previous document. Popping to root and pushing again
/// as two separate state updates (so SwiftUI processes them as two render
/// passes) is the reliable version of the same operation.
private func selectDocument(_ collection: OutlineCollection, _ document: OutlineDocument) {
selectedCollection = collection
documentPath = []
Task { @MainActor in
documentPath = [document]
}
}
@ViewBuilder
private var detail: some View {
if let apiClient = session.apiClient {
// A fresh NavigationStack per collection (or when entering/leaving
// search), so switching either also clears any pushed document.
NavigationStack(path: $documentPath) {
Group {
if !trimmedGlobalQuery.isEmpty {
GlobalSearchResultsView(apiClient: apiClient, query: trimmedGlobalQuery)
} else if let selectedCollection {
CollectionOverviewView(
apiClient: apiClient,
collection: selectedCollection,
searchQuery: $contextualSearchQuery
)
} else {
ContentUnavailableView(
"No Collection Selected",
systemImage: "sidebar.left",
description: Text("Choose a collection to see its documents.")
)
}
}
.navigationDestination(for: OutlineDocument.self) { document in
DocumentReaderView(apiClient: apiClient, document: document)
}
}
.id(trimmedGlobalQuery.isEmpty ? (selectedCollection?.id ?? "none") : "search")
} else {
ContentUnavailableView("Not Signed In", systemImage: "person.crop.circle.badge.exclamationmark")
}
}
}
#endif