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:
@@ -61,6 +61,15 @@ public struct BannerStripView: View {
|
||||
}
|
||||
}
|
||||
.background(.quaternary)
|
||||
// **The strip is an accessibility element** (10-accessibility.md ▸ Live board
|
||||
// announcements, which makes the live-reload-resilience banner one by name). `.contain`
|
||||
// rather than `.combine`: the rows stay individually focusable — each is already one
|
||||
// element with its own tone-prefixed label, and a strip that fused three conditions into
|
||||
// one utterance would bury the lock under the signposts. The group label is what a
|
||||
// VoiceOver user hears on entering it, so "there are conditions here" arrives before the
|
||||
// conditions do.
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityLabel("Board status")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,10 +145,15 @@ private struct BannerRowView: View {
|
||||
.background(row.tone.fill)
|
||||
// One element per row, tone included: a VoiceOver user must hear *that* this is an error
|
||||
// before hearing what the error is, and colour cannot carry that. The dismiss and Cancel
|
||||
// buttons survive as custom actions of the combined element rather than as separate stops
|
||||
// (10-accessibility.md; the announce-on-appear path arrives with that milestone).
|
||||
// buttons survive as custom actions of the combined element rather than as separate stops.
|
||||
//
|
||||
// The label is composed by `AccessibilityPhrases` rather than spelled here because the two
|
||||
// standing conditions are also *announced* on arrival and clearance (10-accessibility.md ▸
|
||||
// Live board announcements, via `BoardAnnouncer`): the sentence a user hears when the lock
|
||||
// appears and the sentence they read off the row afterwards are one string, or they are two
|
||||
// descriptions of one condition waiting to disagree.
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel(Text("\(row.tone.accessibilityPrefix): \(row.headline)"))
|
||||
.accessibilityLabel(Text(AccessibilityPhrases.bannerLabel(tone: row.tone, headline: row.headline)))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -210,16 +224,6 @@ private extension BannerTone {
|
||||
case .info: AnyShapeStyle(.background.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
/// What VoiceOver says before the headline. "Status" rather than "Info" because that is the
|
||||
/// word the platform uses for a non-alarming state announcement.
|
||||
var accessibilityPrefix: String {
|
||||
switch self {
|
||||
case .error: "Error"
|
||||
case .warning: "Warning"
|
||||
case .info: "Status"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
Reference in New Issue
Block a user