Realign search, clipboard, and lane-hover code with the second batch
Creation now clears the search by mechanism, not gesture: one seam (noteUserCreation) states 04's rule once, called from the placeholder funnel, paste — cards and lanes, after the staleness guard so a stale paste clears nothing — and Finder file-drop creation; the attach path deliberately doesn't clear, and cross-board arrivals and New Lane stay outside the seam (a transfer isn't creation; a lane can't be born invisible). An open inline rename now survives the filter hiding its card: the model already kept the editor, but the field renders in the card's slot, so renderedCards keeps the renaming card's slot exactly as long as the editor is open — the query stands throughout, and commit or Escape lets the predicate apply in the same pass. Verified conformant and newly pinned: query-emptied lanes keep their slot with a 0 badge, pasteboard staleness (takeover before paste and mid-staging both no-op), out-transition reachability and the strip pre-divide hold by construction with comments citing their rulings. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -1723,6 +1723,14 @@ public final class BoardStore {
|
||||
/// string, since a missing key is the untitled state and `""` would be a real, blank title
|
||||
/// (01-storage-format.md § Frontmatter).
|
||||
///
|
||||
/// **A Finder file drop is a user-initiated creation, so it clears the search**
|
||||
/// (04-interactions.md § Search, stated by mechanism: "⌘N, Return-creation, the header button,
|
||||
/// empty-space double-click, paste, and Finder file drops alike"). Cleared at the gesture, in
|
||||
/// front of the write, exactly as the placeholder's begin clears it in front of the typing —
|
||||
/// `TransientBoardState.noteUserCreation()` is the rule's one home, and the *attach* half of the
|
||||
/// same gesture (`importAttachments`) deliberately does not call it, because a drop on a card
|
||||
/// creates nothing that could be born invisible.
|
||||
///
|
||||
/// **Partial failure is honest, and leaves no half-made card.** The batch stops at the first file
|
||||
/// that cannot be imported — an unreadable source, a vanished one, a disk with no room left —
|
||||
/// which banners naming it; the cards already made keep their files, matching `importAttachments`'
|
||||
@@ -1735,6 +1743,8 @@ public final class BoardStore {
|
||||
let lane = snapshot.lanes.first(where: { $0.id == laneID && !$0.isDeleted })
|
||||
else { return }
|
||||
|
||||
transient.noteUserCreation()
|
||||
|
||||
let rendered = lane.cards.filter { !$0.isDeleted }
|
||||
let target = min(max(0, index), rendered.count)
|
||||
let root = rootURL
|
||||
|
||||
@@ -257,6 +257,15 @@ public struct NewCardPlaceholder: Sendable, Equatable {
|
||||
/// lives"; the commit re-derives the folder from the current snapshot, so a card an agent filed
|
||||
/// into another lane mid-typing is still renamed correctly.
|
||||
///
|
||||
/// **The filter never touches it** (04-interactions.md § Search, settled): "an open inline rename
|
||||
/// survives the filter hiding its card — the editor is a surface the filter doesn't reach; it stays
|
||||
/// open and focused, commits by UUID wherever the card lives, Escape abandons … and the
|
||||
/// vanish-discard rule stays reserved for true liveness flips". Two halves, in two places: this
|
||||
/// editor outlives a reload that stops its card matching, because `resolve(against:)` discards only
|
||||
/// on a *vanish* and `constrainToSearch(in:)` touches the selection and nothing else; and its
|
||||
/// *field* outlives it too, because `LaneView.rendered` keeps the renaming card's slot in the
|
||||
/// masonry for as long as the editor is open.
|
||||
///
|
||||
/// There is no phase enum. The placeholder needs one because it outlives its own commit (the
|
||||
/// overlay stands in for a card that has not arrived yet); a rename has nothing to stand in for —
|
||||
/// the item is already on screen, and the commit's round trip simply updates it.
|
||||
@@ -498,6 +507,44 @@ public final class TransientBoardState {
|
||||
lastActiveLaneID = laneID
|
||||
}
|
||||
|
||||
// MARK: - Creation's carve-out
|
||||
|
||||
/// **A user-initiated creation clears the query** — 04-interactions.md § Search's one exception
|
||||
/// to the pure predicate, and the single place it is stated.
|
||||
///
|
||||
/// > creating a card clears the search — creation's carve-out exists because a brand-new card
|
||||
/// > must not be born invisible, and it is **stated by mechanism, not by gesture** (settled):
|
||||
/// > *any* user-initiated creation on the board clears the query — ⌘N, Return-creation, the
|
||||
/// > header button, empty-space double-click, paste, and Finder file drops alike — while
|
||||
/// > foreign/agent-filed cards keep riding the live filter.
|
||||
///
|
||||
/// So this is a *seam*, not a gesture's line: every path that mints an item because the user
|
||||
/// asked for one calls it, and there are exactly three of them —
|
||||
///
|
||||
/// - `beginPlaceholder(inLane:after:)` below, which is itself the funnel for the four inline
|
||||
/// creation gestures (⌘N, Return on a lane, the header button, a double-click on empty space);
|
||||
/// - `ClipboardStore.perform` — ⌘V, cards and lanes alike, at the moment the items actually
|
||||
/// land (a paste the pasteboard went stale under lands nothing and so clears nothing);
|
||||
/// - `BoardStore.createCards(fromFiles:inLane:at:)` — the Finder file drop's *create* half. Its
|
||||
/// attach half (`importAttachments`) deliberately does not call this: dropping files on a card
|
||||
/// creates nothing, so there is no card to be born invisible.
|
||||
///
|
||||
/// **What is deliberately not here** is the other half of the same sentence: an item this board
|
||||
/// receives without the user asking *it* for one keeps riding the filter. A cross-board drag
|
||||
/// arrival is the near miss — it is a transfer whose destination-side clear 04 does not state,
|
||||
/// and the enumerated mechanisms above are the ones it does — and an agent filing a card is the
|
||||
/// far one (02-architecture.md's derived-result rule). **New Lane is not here either**, for the
|
||||
/// carve-out's own reason rather than in spite of it: "lanes are never filtered out", so a lane
|
||||
/// cannot be born invisible and has nothing to be rescued from — ⇧⌘N under a query adds a lane
|
||||
/// showing a `0` badge, which is the filter behaving exactly as designed.
|
||||
///
|
||||
/// Widening the universe invalidates nothing, so unlike a *narrowing* write this needs no
|
||||
/// snapshot and no `constrainToSearch(in:)` behind it — which is why the rule can live on this
|
||||
/// type at all rather than on the store (see `searchQuery`).
|
||||
public func noteUserCreation() {
|
||||
searchQuery = ""
|
||||
}
|
||||
|
||||
// MARK: - The placeholder's lifecycle
|
||||
|
||||
/// Opens the inline editor for a new card in `laneID`, replacing any editor already open and
|
||||
@@ -511,10 +558,11 @@ public final class TransientBoardState {
|
||||
/// would ordinarily *commit*, but that rule is about focus leaving for the board, and here the
|
||||
/// focus is being taken by another editor before the user has said they are done.
|
||||
///
|
||||
/// **Creation clears the search, and this is the funnel** (04-interactions.md § Search): "a
|
||||
/// brand-new card must not be born invisible". Every entry point to creation goes through here
|
||||
/// — ⌘N, Return on a lane, the lane header's button, a double-click on empty space — so the
|
||||
/// carve-out is stated once instead of four times.
|
||||
/// **Creation clears the search, and this is the funnel for the inline gestures**
|
||||
/// (04-interactions.md § Search): "a brand-new card must not be born invisible". ⌘N, Return on a
|
||||
/// lane, the lane header's button and a double-click on empty space all arrive here, so the four
|
||||
/// of them state the carve-out once — by calling the rule's one home, `noteUserCreation()`,
|
||||
/// which the paste and Finder-file-drop paths call too.
|
||||
///
|
||||
/// **Rename deliberately gets no such line** (04, settled): "the filter stays a pure predicate
|
||||
/// with one exception, not two". A rename committed under an active search re-runs the
|
||||
@@ -526,7 +574,7 @@ public final class TransientBoardState {
|
||||
/// rule), or `nil` for the lane's bottom — which is what Return, the header button, and a
|
||||
/// double-click on empty space all pass.
|
||||
public func beginPlaceholder(inLane laneID: ItemID, after anchorCardID: ItemID? = nil) {
|
||||
searchQuery = ""
|
||||
noteUserCreation()
|
||||
renameEditor = nil
|
||||
newCardPlaceholder = NewCardPlaceholder(laneID: laneID, anchorCardID: anchorCardID)
|
||||
noteActiveLane(laneID)
|
||||
@@ -722,10 +770,14 @@ public final class TransientBoardState {
|
||||
/// step from somewhere the user cannot see. Neither has to stay *in* the selection — that
|
||||
/// asymmetry is `resolve`'s and survives here untouched.
|
||||
///
|
||||
/// **The drag and the pending cut are deliberately left alone.** 04 hides cards and says one
|
||||
/// thing about the consequence — that they leave the *selection*. A cut is staged content
|
||||
/// waiting for a paste that may well happen after the search clears, and a drag under a live
|
||||
/// filter is a gesture in flight, not a set the filter has any claim on.
|
||||
/// **The drag, the pending cut and the rename editor are deliberately left alone.** 04 hides
|
||||
/// cards and says one thing about the consequence — that they leave the *selection*. A cut is
|
||||
/// staged content waiting for a paste that may well happen after the search clears, and a drag
|
||||
/// under a live filter is a gesture in flight, not a set the filter has any claim on. The
|
||||
/// editor's absence is the settled ruling in person: "an open inline rename survives the filter
|
||||
/// hiding its card … the vanish-discard rule stays reserved for true liveness flips"
|
||||
/// (`RenameEditor`), so a foreign edit that stops the renaming card matching drops it from the
|
||||
/// selection here and leaves the keystrokes exactly where the user left them.
|
||||
public func constrainToSearch(in snapshot: BoardModel) {
|
||||
let filter = SearchFilter(query: searchQuery)
|
||||
guard filter.isActive else { return }
|
||||
|
||||
Reference in New Issue
Block a user