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
+8 -5
View File
@@ -74,12 +74,15 @@ struct DragPayloadTests {
@Suite("DragLocality")
struct DragLocalityTests {
private static let here = URL(fileURLWithPath: "/Boards/Work.kanban", isDirectory: true)
private static let there = URL(fileURLWithPath: "/Boards/Home.kanban", isDirectory: true)
// Instance members, not `static`: every case below names them bare, and a static member is not
// reachable unqualified from an instance method. Swift Testing builds a fresh instance per test,
// so these are as constant either way.
private let here = URL(fileURLWithPath: "/Boards/Work.kanban", isDirectory: true)
private let there = URL(fileURLWithPath: "/Boards/Home.kanban", isDirectory: true)
private static let none: NSEvent.ModifierFlags = []
private static let option: NSEvent.ModifierFlags = [.option]
private static let command: NSEvent.ModifierFlags = [.command]
private let none: NSEvent.ModifierFlags = []
private let option: NSEvent.ModifierFlags = [.option]
private let command: NSEvent.ModifierFlags = [.command]
@Test("Roots compare by their standardized path, so the same board is the same board")
func rootComparison() {