import AppKit // MARK: - AccessibilityAnnouncer /// **The app's one `NSAccessibility.post` call site** — the thin, untestable half of every spoken /// announcement (10-accessibility.md ▸ Live board announcements, ▸ Trash lane). /// /// Everything *decidable* about an announcement — whether one is owed, which of several competing /// sentences wins, and what it says — is a pure function elsewhere (`BoardAnnouncer`, /// `AccessibilityPhrases`). What is left is two lines of AppKit that cannot be asserted about /// without a screen reader attached, so they are stated once, here, rather than copied to every /// producer where they could quietly drift apart. /// /// **Medium priority, always.** 10 asks for "one polite (non-interrupting) digest"; `.high` cuts off /// speech already in progress, which is right for a modal failure and wrong for every announcement /// this app makes — a board narrating an agent's edit must never talk over the sentence the user is /// currently listening to. /// /// **Posted to the key window** so the announcement is attributed to the board the user is looking /// at rather than to the application at large; `NSApplication.shared` is the fallback for the moment /// between windows, where an attributed announcement is still better than a dropped one. enum AccessibilityAnnouncer { /// Says `phrase`, or says nothing. /// /// `nil`-and-empty-tolerant on purpose: every producer above it answers with an *optional* /// sentence — "one announcement per reload, and usually none" is the whole shape of /// `BoardAnnouncer.speech(for:)` — so the `nil` check belongs here rather than at each call /// site, where forgetting it would post an empty announcement (a spoken pause) instead of /// staying quiet. @MainActor static func post(_ phrase: String?) { guard let phrase, !phrase.isEmpty else { return } let element: Any = NSApplication.shared.keyWindow ?? NSApplication.shared NSAccessibility.post( element: element, notification: .announcementRequested, userInfo: [ .announcement: phrase, .priority: NSAccessibilityPriorityLevel.medium.rawValue ] ) } }