Implement live accessibility announcements
The board speaks when files change under the user, per DESIGN/10 § Live board announcements. BoardDiff is the pure snapshot summarizer (identity sets for cards/lanes added/edited/moved/deleted — ids, not tallies, so pro-m1's semantic commit engine can build on it; edited = rendered content only, moved beats edited, implied events don't steal the subject). BoardAnnouncer is the decision seam: focusOutcome computes the vanishing-focus sentence and the walk-up-then-sideways recovery (next lane by order, else previous, board container only when none remain, never the trash); speech(for:) is the one-sentence precedence ladder — raised condition > bracket completion > cleared condition > vanished focus > digest — foreign-only for the last two rungs, so app-mediated echoes stay silent. BoardStore.land assembles ReloadFacts and posts exactly one sentence per reload through the injectable announce outlet (AccessibilityAnnouncer, medium priority, never interrupting). Selection recovery layers on top of ItemReferenceSet re-resolution — survivors veto, the emptied selection lands on the vanished item's lane and re-arms ⌘N's active-lane memory. performWholesale(announcing:) arms a completion phrase consumed by the closing reload — nil on every base bracket today; pro-m1 fills git phrasings. Locks raised outside the reload path (vanished root, unwritable location) announce through the same ladder, and the banner strip is a labeled "Board status" container whose row labels are the announced sentences (AccessibilityPhrases.bannerLabel — one string for eye and ear). Announcements classify at reload granularity (WatchOrigin) as a deliberate interim: DESIGN/02's EchoLedger (per-file classification, the announcer's specified input, git-free) was scheduled with the auto-committer that the edition split moved to pro-m1 — filed on the Redesign board for a ruling. 1533 unit tests green, both schemes build. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import AppKit
|
||||
import Observation
|
||||
import SwiftUI
|
||||
|
||||
@@ -291,32 +290,17 @@ extension BoardStore {
|
||||
clearSelection()
|
||||
}
|
||||
}
|
||||
announceTrashVisibility(shown)
|
||||
}
|
||||
|
||||
/// **"Toggling visibility is announced"** (10-accessibility.md ▸ Trash lane).
|
||||
///
|
||||
/// A whole container joins or leaves the accessibility tree here and nothing else marks it: the
|
||||
/// VoiceOver cursor does not move, no focus is lost, and the re-divide every lane performs is
|
||||
/// silent by nature. Announced from the store rather than from either caller for
|
||||
/// `setTrashVisible`'s own reason — the View menu row and the toolbar item are one command with
|
||||
/// two faces, and a consequence written at one of them would be missing from the other.
|
||||
///
|
||||
/// One post, and deliberately no machinery around it: the live board's announcements — foreign
|
||||
/// edits, vanishing focus, bracketed operations — are their own design (10 ▸ Live board
|
||||
/// announcements) with a summarizer and a debounce behind them, and this is not an instalment of
|
||||
/// that. Posted to the key window so it is attributed to the board the user is looking at, at
|
||||
/// medium priority: informative, and not worth interrupting speech already in progress.
|
||||
private func announceTrashVisibility(_ shown: Bool) {
|
||||
let element: Any = NSApplication.shared.keyWindow ?? NSApplication.shared
|
||||
NSAccessibility.post(
|
||||
element: element,
|
||||
notification: .announcementRequested,
|
||||
userInfo: [
|
||||
.announcement: AccessibilityPhrases.trashVisibility(shown: shown),
|
||||
.priority: NSAccessibilityPriorityLevel.medium.rawValue
|
||||
]
|
||||
)
|
||||
// **"Toggling visibility is announced"** (10-accessibility.md ▸ Trash lane). A whole
|
||||
// container joins or leaves the accessibility tree here and nothing else marks it: the
|
||||
// VoiceOver cursor does not move, no focus is lost, and the re-divide every lane performs is
|
||||
// silent by nature. Announced from the store rather than from either caller for
|
||||
// `setTrashVisible`'s own reason — the View menu row and the toolbar item are one command
|
||||
// with two faces, and a consequence written at one of them would be missing from the other.
|
||||
//
|
||||
// Deliberately **not** on the reload's one-announcement-per-debounce ladder
|
||||
// (`BoardAnnouncer`): this is a view state the user just toggled, not a change to the board's
|
||||
// files, so there is no origin to classify and nothing for it to compete with.
|
||||
announce(AccessibilityPhrases.trashVisibility(shown: shown))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user