import SwiftUI /// Presents `DirtyBufferGuard`'s blocked phase as the app's one modal moment on the write-failure /// path (02-architecture.md § Write-failure surfacing). /// /// ### The three choices, and why there is no fourth /// /// Retry, save a copy elsewhere, discard. There is deliberately **no Cancel** — no "keep the window /// open and think about it": the close is already stopped, so a fourth button would only mean /// "stop asking", which is the silent failure this alert exists to prevent. Dismissing the alert /// without choosing leaves the phase blocked and the alert comes back; the only ways out are the /// three that put the text somewhere or knowingly let it go. /// /// ### The copy destination /// /// `copyDestination` supplies the URL. In a real window that is an `NSSavePanel` (or a /// `fileExporter`) run from the button; here it is a closure so the flow is testable and so this /// modifier stays free of file-picking machinery. Returning `nil` means the user backed out of the /// panel — the phase stays blocked and the alert returns, which is the honest outcome. /// /// ### Callers /// /// m6's editor sessions (the card window's body and raw-source buffers) and m4's board-close /// flush. Both attach this to the window that is trying to close. public extension View { func dirtyBufferAlert( _ bufferGuard: DirtyBufferGuard, copyDestination: @escaping @MainActor () -> URL? ) -> some View { modifier(DirtyBufferAlertModifier(bufferGuard: bufferGuard, copyDestination: copyDestination)) } } private struct DirtyBufferAlertModifier: ViewModifier { let bufferGuard: DirtyBufferGuard let copyDestination: @MainActor () -> URL? func body(content: Content) -> some View { content.alert( "Your changes couldn't be saved", isPresented: Binding( // A getter over the phase and a setter that does nothing: SwiftUI writes `false` // when the alert is dismissed by any route it manages, and honouring that would // close the window with the text still nowhere but memory. Only the three buttons // move the phase, so only they can take the alert down. get: { bufferGuard.phase != .idle }, set: { _ in } ), presenting: blockingError ) { _ in Button("Try Again") { bufferGuard.retry() } Button("Save a Copy…") { guard let url = copyDestination() else { return } // A failed copy rethrows into a still-blocked phase, so the alert simply returns — // the same place the user already was, with nothing lost. There is no second error // surface to build here: this *is* the error surface. try? bufferGuard.saveCopy(to: url) } Button("Discard Changes", role: .destructive) { bufferGuard.discard() } } message: { error in Text("\(BannerCenter.headline(for: error))\n\nSave a copy somewhere else, or discard the changes to close.") } } private var blockingError: BoardWriteError? { if case let .blocked(error) = bufferGuard.phase { error } else { nil } } }