Implement live search filtering
The board's live title+body filter per 04-interactions.md § Search: - SearchFilter — a pure value folding the query once (case- and diacritic-insensitive substring, locale-stable); title OR body matches, attachment filenames never searched; only the literal empty string is inactive. - One universe: the filter threads through SelectionGrammar's order lists as a defaulted parameter, so ranges, Select All, arrow navigation, the marquee, drop zones, count badges, and the shown trash all read the same filtered set by construction; lanes are deliberately never filtered out (an emptied lane keeps its slot with a 0 badge). Hidden cards leave the selection through the existing constrain primitive, run on every query change and as the last line of the reload resolve; the delete successor is filtered so ⌫ never selects a hidden neighbour. - The field: an NSSearchField-backed toolbar item (the toolbar's sole default item); Edit ▸ Find ⌘F focuses it through a focused-value presentation; stock field-editor dispatch — Return swallowed, Tab is the keep-filter path to the board, board commands stay enabled except the caret-chord pair, now one shared caretChordsYield expression. - Escape is staged: clear the non-empty query (focus stays), hand an empty field back to the board, clear an active search from board focus — before Escape's clear-selection meaning. - Creating a card clears the search (the placeholder funnel); a rename deliberately gets no carve-out; filter reflow rides the content spring keyed narrowly on the query. 903 unit tests (24 new). Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
+20
-10
@@ -109,8 +109,14 @@ enum Motion {
|
||||
/// restore reads like the search filter — leavers and arrivers run their transition, survivors
|
||||
/// reflow under one gentle spring".
|
||||
///
|
||||
/// Named ahead of both its call sites (m5's search, m7's undo) for the same reason `delete` is:
|
||||
/// the figure is settled, and a duration that has nowhere to live gets spelled at a call site.
|
||||
/// **The search filter is its call site** (`BoardView.laneStrip`), where it wraps a transaction
|
||||
/// keyed on the query and nothing else — 03's own narrow key for this reflow. The leavers and
|
||||
/// arrivers it talks about are `cardTransition`, already attached to every card slot and trash
|
||||
/// row, so the two halves of the sentence are two modifiers rather than one bespoke animation.
|
||||
///
|
||||
// m7-undo: the restore is the other half of the pair. It arrives as a bracketed wholesale
|
||||
// reload, so it reaches this voice through `reloadAnimation` rather than through a transaction
|
||||
// of its own — see `reloadAnimates(origin:endsBracketedOperation:)`.
|
||||
static func contentReflow(reduced: Bool) -> Animation? {
|
||||
reduced ? nil : .smooth(duration: Duration.contentReflow)
|
||||
}
|
||||
@@ -145,8 +151,9 @@ enum Motion {
|
||||
reduced ? .crossfade : .scaleAndFade(from: AppearScale.lane)
|
||||
}
|
||||
|
||||
/// A card arriving or leaving — a create, a delete, a Put Back, and (m5) a search filter's
|
||||
/// leavers and arrivers. The trash's rows wear it too: they are cards, and 10 requires the trash
|
||||
/// A card arriving or leaving — a create, a delete, a Put Back, and the search filter's leavers
|
||||
/// and arrivers ("Search-hiding rides the same structural transition — hiding is removal, not a
|
||||
/// special fade"). The trash's rows wear it too: they are cards, and 10 requires the trash
|
||||
/// animations to have a reduced variant like everything else.
|
||||
static func cardTransition(reduced: Bool) -> AnyTransition {
|
||||
cardAppearance(reduced: reduced).transition
|
||||
@@ -243,12 +250,15 @@ enum Motion {
|
||||
/// The thin wrapper `BoardStore.land` hands to `withAnimation`: the voice a landing snapshot is
|
||||
/// applied in, or `nil` for the reloads that snap.
|
||||
///
|
||||
// m5-drag, m5-search: the voice is the *general* structural spring for every app-mediated
|
||||
// reload, because the reload seam knows an operation echoed but not which one — 03 gives delete
|
||||
// 0.25 s and a drop commit its own dialect, and neither is reachable from an origin tag. The
|
||||
// per-operation figures (`delete`, `dragReflow`) become reachable when the operations that own
|
||||
// them run their own transactions around the gesture, which is m5's card; this seam stays the
|
||||
// floor under them.
|
||||
// m5-drag: the voice is the *general* structural spring for every app-mediated reload, because
|
||||
// the reload seam knows an operation echoed but not which one — 03 gives delete 0.25 s and a
|
||||
// drop commit its own dialect, and neither is reachable from an origin tag. The per-operation
|
||||
// figures (`delete`, `dragReflow`) become reachable when the operations that own them run their
|
||||
// own transactions around the gesture; this seam stays the floor under them.
|
||||
//
|
||||
// The search filter needed none of that and never reaches here: a query change is not a reload
|
||||
// at all — it is transient state, so its transaction is wrapped where it happens
|
||||
// (`BoardView.laneStrip`, `contentReflow`).
|
||||
static func reloadAnimation(origin: WatchOrigin, endsBracketedOperation: Bool, reduced: Bool) -> Animation? {
|
||||
guard reloadAnimates(origin: origin, endsBracketedOperation: endsBracketedOperation) else { return nil }
|
||||
return structural(reduced: reduced)
|
||||
|
||||
Reference in New Issue
Block a user