Phase 3 finishes the pivot at the surface. One card face serves two
containers: CardFaceView extracted with a role — board or trash — so
stripe, tint, chip, selection stroke, cut dim, marquee registration,
and drag are shared by construction, the trash side differing only in
its absences: no Open, no rename, no Style, no file-hover highlight,
and a Delete that goes through the confirmation host. The column
rewrote around the lanes' own single-column masonry so drag reflow
reads as positional slides; chrome stays the hatched header, symbol,
and count — 11 gives Empty Trash to the File menu alone. Two real
grammar bugs die here: plain Backspace on a trash selection purged
without the confirmation the menu raises, and the context menu's
Delete resolved against the standing selection, so right-clicking a
trash card under a board selection silently did nothing — it now
stages the clicked set explicitly. Open, Rename, Style, and Empty
Trash validation became testable store seams; the column is one named
accessibility container of ordinary card elements. The tombstone era
is swept: deleteItem, restoreItem, stripTombstonedChildren — dead
since lane copies stopped nesting trash — the restore verb, the
unreachable put-back banner row, and every quasi-lane doc comment.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
NSToolbar through the existing HostedWindowController rather than
SwiftUI's toolbar — for reasons that are contract, not taste: 03's
transient-search clause is a decision over the toolbar's current
contents, which NSToolbar publishes and SwiftUI's API cannot answer;
Undo/Redo are the system's nil-target responder-chain actions so the
toolbar items validate exactly as the menu rows do (disabled on base
boards, alive in m8 unchanged); and the search item hosts the real
NSSearchField with explicit first-responder control. Customization is
all system furniture — Customize sheet, drag rearrange, display-mode
popup, overflow, autosaved per window kind. Board default: the search
field alone, trailing; catalog adds New Card, New Lane, Undo, Redo,
Show Trash, every action extracted from its menu command so no second
predicate exists. Card default: the Edit Body / Raw Source toggles and
Add Attachment, mirroring their commands' own predicates live via
observation tracking. Removing the search item keeps the promise —
⌘F surfaces the same field as a transient strip under the title bar,
persisting until the query clears, and an overflowed item that cannot
take the keyboard falls through to the strip too.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
03's resettlement reverses the pathfinder carry-over: the
selection-keyed dual presentation proved undesirable, so a card has one
presentation — selection changes styling, never geometry, and the
masonry never reflows on click. Deleted the carousel view (page dots,
glass underlay, scroll-tick monitor), the QuickLook thumbnail cache
(sole consumer), the pure paging/suppression rules, and the
sole-selected animation key — Motion now keys transactions on the
search query and the drop proposal only. The attachment chip stays as
the face's whole attachment story; viewing media is the card window's
job. No carousel state had leaked beyond the view layer.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
The card face's in-place expansion (03-board-ui.md § Card face): a card
with attachments, as the sole selection, grows a paged media band below
its title; every other state stays compact behind the quiet paperclip.
- CardCarousel owns the pure rules: the sole-selection predicate decides
by identity (a sole-selected lane matches no face, no snapshot walk),
and expansion is suppressed outside the animation key while a rubber
band is active — a band names a set in progress, so carousels neither
flicker nor animate under it.
- QuickLook thumbnails generate off-main into a per-window cache keyed to
survive reselection, with the Finder-icon fallback while loading and
for non-previewable types; pages ride the platform paging behavior,
dots (glass underlay, solid under Reduce Transparency) click to page,
and a local wheel monitor turns a discrete tick into one clamped page —
precise trackpad pans fall through untouched.
- The expansion animates under Motion's new carouselExpansion transaction
keyed narrowly on the sole-selected card; Reduce Motion goes instant.
Drop-slot math and the marquee read the expanded height for free — both
re-register on every size change.
928 unit tests (24 new). m5-interactions complete.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
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
One named home for every curve and duration — the two-voice split 03
fixes (snappy structural: drag reflow 0.18, delete 0.25, lane resize
0.2; smooth content-reflow 0.28), the scale-and-fade appear/disappear
transitions (cards 0.8, lanes 0.9), and a Reduce Motion variant on
every accessor (animations go instant, transitions go crossfade). A
post-migration grep holds the invariant: zero motion literals outside
Motion.swift. The animate-vs-snap split lands where Lanework's
one-way flow puts it: the store's reload seam. An app-mediated echo
applies its snapshot inside the structural transaction; foreign and
reconciling reloads — and every bracket-ending wholesale reload —
assign bare, because live-reload is the board becoming what's on
disk, not an event to perform. A window that merged foreign events
into an app-mediated span animates, deliberately: the ratified merge
rule makes it indistinguishable from a pure echo, and a test pins
that reading so a future mixed case fails loudly. The lane-reorder
reflow keys on the drop proposal alone (pointer tracking stays 1:1),
the resize session freezes its Reduce Motion answer at drag start so
the unit tick and the window resize can never disagree, and the m5
drag / m5 search / m7 undo voices are named seams waiting for their
call sites. 13 new tests.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY