Comments, phase 3 — search, the thread find, announcements, and a11y
Board search reaches comment bodies through a search-owned transient
index: the first live-query keystroke sweeps comments/*/index.md
off-actor (.draft and comments/.trash excluded), keystrokes re-filter
in memory, the index discards on clear — the snapshot stays O(cards).
⌘F routes by focus: the comments pane gets an app-owned find bar
spanning the whole rendered thread (next/prev cross rows with
wraparound); body and composer keep NSTextFinder; Find Next/Previous
graduate from FutureCommands. Foreign comment changes speak
path-shaped beside the announcer's ladder ("New comment on 'X'",
plural folds), narrowed by EchoLedger receipts consumed through
CommentPath.classify — and that read fixed a latent footprint bug
where a comment receipt resolved against the card's attachment
listing, read .absent, and classified the user's own write as
foreign. The pane completes its a11y story: flattened comment
elements with Edit/Delete/Reveal custom actions (un-flattening
during inline edit), phrase-table vocabulary, labeled composer and
sort control, and an audit over the open pane on a comment-seeded
fixture (runnable only where automation permission exists).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -362,6 +362,11 @@ struct CardWindowHost: View {
|
||||
attachments.cardFolder = folder
|
||||
session.comments.cardFolder = folder
|
||||
}
|
||||
// The announcer's subject, re-derived from every snapshot for the folder's reason: a card
|
||||
// renamed mid-session is announced under its new name ("New comment on '⟨card⟩'").
|
||||
.onChange(of: placement.card.title.value, initial: true) { _, title in
|
||||
session.comments.cardTitle = title
|
||||
}
|
||||
.onChange(of: store.isReadOnly, initial: true) { _, locked in
|
||||
attachments.isEditable = !locked
|
||||
session.comments.isEditable = !locked
|
||||
@@ -372,13 +377,15 @@ struct CardWindowHost: View {
|
||||
// *Any* reload, not a filtered one, and that is a deliberate choice worth stating: the
|
||||
// store's observable surface publishes `snapshotGeneration` and a `BoardModel` — it does
|
||||
// not vend the changed paths, and comments are outside the snapshot entirely
|
||||
// (01-storage-format.md § Enhanced schema), so there is nothing here to run
|
||||
// `CommentPath.classify` against. Re-reading one card's thread is a handful of small
|
||||
// files and happens only while a card window is open; filtering would mean either
|
||||
// widening the store's surface to carry paths, or the pane keeping its own watcher — a
|
||||
// second stream over the same tree, which the one-way flow rules out. `initial:` is
|
||||
// deliberately absent: `start()` already did the opening read, after the residue sweep
|
||||
// that has to precede it.
|
||||
// (01-storage-format.md § Enhanced schema), so there is nothing to filter *on* here.
|
||||
// Re-reading one card's thread is a handful of small files and happens only while a card
|
||||
// window is open; filtering would mean either widening the store's surface to carry paths,
|
||||
// or the pane keeping its own watcher — a second stream over the same tree, which the
|
||||
// one-way flow rules out. The path shape is read on the other side of the re-read instead,
|
||||
// where there *are* two pictures to compare: the pane diffs its threads and consumes the
|
||||
// ledger's comment receipts through `CommentPath.classify` to tell a foreign arrival from
|
||||
// its own echo (`CardComments.reload`). `initial:` is deliberately absent: `start()`
|
||||
// already did the opening read, after the residue sweep that has to precede it.
|
||||
.onChange(of: store.snapshotGeneration) { _, _ in
|
||||
session.comments.reload()
|
||||
}
|
||||
@@ -577,6 +584,13 @@ struct CardWindowHost: View {
|
||||
comments.composer.post = { [weak store] in
|
||||
store?.postComment(inCard: cardID)
|
||||
}
|
||||
// The announcer's gate: which of this thread's changes the app itself wrote, consumed once per
|
||||
// reload (10-accessibility.md — "app-mediated echoes never announce", per comment). A store
|
||||
// that has gone vouches for nothing, which is the conservative direction and also the one the
|
||||
// announcement cannot reach anyway — a released store has no window left to speak in.
|
||||
comments.vouchedComments = { [weak store] in
|
||||
store?.vouchedComments(inCard: cardID) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/// Points the attachments section at its card — **the one place Add Attachment… and Remove
|
||||
|
||||
Reference in New Issue
Block a user