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:
@@ -194,11 +194,16 @@ extension FocusedValues {
|
||||
/// **A board window in front is now the whole of the scope**, where m5 additionally required the
|
||||
/// toolbar's field to exist: with the item removed, ⌘F raises the transient host instead
|
||||
/// (`BoardSearchPresentation.invokeSearch`), so there is no board window where the row is dead.
|
||||
/// **The card window's find routes by focus** (05-card-window.md ▸ Preview, comments clause): the
|
||||
/// body's substrate when the body has the keyboard, the whole rendered thread when the comments pane
|
||||
/// does, and that editor's own stock find bar when the composer or an inline session does. The rule
|
||||
/// itself is `CardWindowFind.route`, pure and pinned; this row only asks it.
|
||||
struct FindCommand: View {
|
||||
|
||||
@FocusedValue(\.boardStore) private var store
|
||||
@FocusedValue(\.boardSearch) private var search
|
||||
@FocusedValue(\.cardBody) private var cardBody
|
||||
@FocusedValue(\.cardComments) private var comments
|
||||
|
||||
/// **The card window wins when it is the focused scene**, which is the whole of 11's split
|
||||
/// ("Board window: board search; card window: find-in-text"): the two never both publish, so
|
||||
@@ -209,6 +214,12 @@ struct FindCommand: View {
|
||||
|
||||
var body: some View {
|
||||
Button("Find") {
|
||||
// The pane answers first and says whether it took the key: with the comments pane focused
|
||||
// — or its find bar already up — ⌘F is the thread's, and every other case falls through to
|
||||
// the body exactly as it did before the pane existed.
|
||||
if comments?.invokeFind(hasBody: findInText != nil) == true {
|
||||
return
|
||||
}
|
||||
if let findInText {
|
||||
findInText()
|
||||
} else {
|
||||
@@ -216,7 +227,7 @@ struct FindCommand: View {
|
||||
}
|
||||
}
|
||||
.keyboardShortcut("f", modifiers: .command)
|
||||
.disabled(findInText == nil && (store == nil || search == nil))
|
||||
.disabled(findInText == nil && comments == nil && (store == nil || search == nil))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -334,11 +334,16 @@ struct BoardView: View {
|
||||
// arrivers are the card and row transitions already attached inside the lanes and the trash
|
||||
// column; this is the survivors' spring around them.
|
||||
//
|
||||
// **Every way the query changes rides it**, which is the reason the key is the query rather
|
||||
// **Every way the filter changes rides it**, which is the reason the key is the filter rather
|
||||
// than the transaction being wrapped at each mutation: typing, the field's Escape, the
|
||||
// board's Escape, and creation's clear (`TransientBoardState.beginPlaceholder`) all land
|
||||
// here without any of them knowing about motion.
|
||||
.animation(Motion.contentReflow(reduced: reduceMotion), value: store.searchQuery)
|
||||
//
|
||||
// The *filter*, not the query, since comments joined the search's scope (04 ▸ Search, re-ruled
|
||||
// 2026-07-29): a landed comment-index sweep widens what matches without the query changing at
|
||||
// all, and keying on the query alone would make those cards pop in while every other arrival
|
||||
// eased. `SearchFilter` is `Equatable` and carries the index' answer, so one key covers both.
|
||||
.animation(Motion.contentReflow(reduced: reduceMotion), value: store.searchFilter)
|
||||
}
|
||||
|
||||
/// The rubber band itself: a translucent accent fill with a hairline border, in strip
|
||||
|
||||
Reference in New Issue
Block a user