Implement Finder file drops

Files from Finder land on the board per 04-interactions.md § Drag & drop:

- Dropped on a card, they copy into its attachments/ (any type, multi-file),
  the face highlighting while hovered; the hovered card is resolved by
  hit-testing the same analytic masonry frames the card zones are built
  from, so attach-beats-create adds no drop region and cannot drift from
  the dispatch.
- Dropped on lane empty space, one card per file — filename minus extension
  as the title (a blank stem omits the key), fresh GUID, rank at the drop
  position through the ordinary insertion machinery, the file attached —
  all in one bracket; a failed import removes the just-minted card, so
  creating-then-abandoning never leaves an empty card behind.
- Every board drop surface now declares .fileURL beside the two board
  types (the single-target-dispatch rule); tombstoned surfaces are inert;
  file sessions ride a distinct session mode with their own watchdog and
  no hysteresis, leaving the board-drag machinery untouched.
- Also: four empty fixture directories pinned with .keep files so git
  preserves them, and a test-only visibility fix in DragSessionTests.

811 unit tests (12 new).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-27 21:20:46 -04:00
parent 90cf82d740
commit 8f116934b4
13 changed files with 992 additions and 59 deletions
+5 -4
View File
@@ -125,9 +125,10 @@ struct BoardView: View {
.onGeometryChange(for: CGRect.self) { $0.frame(in: .global) } action: { laneDrops.stripFrame = $0 }
// **The strip's drop target** the backdrop, the gaps, the outer margin, and the trash
// column's footprint, which is never a landing spot of its own (04-interactions.md The
// trash) and so simply falls through to here. It accepts *both* board types and routes
// internally, because single-target dispatch has no fall-through (DRAG-REORDER.md).
.onDrop(of: boardDragTypes, delegate: StripDropDelegate(context: dropContext))
// trash) and so simply falls through to here. It accepts *every* session type ours and
// external Finder file drags alike and routes internally, because single-target
// dispatch has no fall-through (DRAG-REORDER.md).
.onDrop(of: boardDropTypes, delegate: StripDropDelegate(context: dropContext))
}
.background(boardBackground)
// The window-level fallback, *behind* the specific targets: a release over any in-window
@@ -135,7 +136,7 @@ struct BoardView: View {
// cancel-snapback the drop lands where the shadows show, which is what the shadows promise.
.background {
Color.clear
.onDrop(of: boardDragTypes, delegate: BoardFallbackDropDelegate(context: dropContext))
.onDrop(of: boardDropTypes, delegate: BoardFallbackDropDelegate(context: dropContext))
}
// **The committed-overlay hold's hand-off** (DRAG-REORDER.md § The committed-overlay hold):
// the overlay stands in for an arrangement that is on disk but not yet in the snapshot, and