Land Finder file drops positionally, header release topmost

04's settled clauses were mostly shipped already — the create landing
resolved through DropSlotMath.cardSlot with one nominal shadow per
importable file — but a release on the lane header fell through to the
card zones, which clamp inward, so a scrolled lane could propose behind
the header stripe. FileDropZones now folds header, attach hit-test, and
card-slot resolution into one pure seam asked in that order, the header
answering topmost per the ruling; lane headers register their frames
for it. FinderDrop.shadowCount names the floor-at-one rule. New tests
pin the header boundary, a differential against cardSlot's own zones
(same zones, not similar), and a store-level differential proving a
file landing takes the very ranks a card move there takes.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 08:28:28 -04:00
parent 7be9bb2345
commit 524488122f
7 changed files with 392 additions and 72 deletions
+72 -10
View File
@@ -278,6 +278,42 @@ struct CreateCardsFromFilesTests {
}
}
/// **"Created cards land at the drop position resolved through the same card-grid zones an
/// ordinary card drag uses"** (04-interactions.md Drag and drop, settled 2026-07-28). The
/// gesture's half of that is `FileDropZones`; the *write*'s half is this: landing at index N must
/// produce the ranks a card dropped at index N would have produced, not merely an order that
/// happens to read right.
@Test("A file landing at index N takes the very rank a card dropped at index N would take")
func ranksMatchACardDropAtTheSameIndex() throws {
for index in 0...3 {
// The file drop: one card minted at `index` in Todo.
let dropped = try makeBoard()
defer { dropped.tearDown() }
let sources = try DropSources()
defer { sources.tearDown() }
let fileStore = try BoardStore(rootURL: dropped.root)
fileStore.createCards(fromFiles: [try sources.file("dropped.txt")], inLane: lane1, at: index)
// The card drop: Fourth dragged out of Doing into the same slot of the same lane. Its
// neighbours are identical, so a shared rank arithmetic must answer identically.
let moved = try makeBoard()
defer { moved.tearDown() }
let cardStore = try BoardStore(rootURL: moved.root)
cardStore.moveCards([ItemID(rawValue: Ident.card4)], toLane: lane1, at: index)
let droppedCards = try cards(lane1, in: dropped)
let movedCards = try cards(lane1, in: moved)
#expect(droppedCards.map(\.title.value)
== movedCards.map { $0.title.value == "Fourth" ? "dropped" : $0.title.value },
"index \(index): the run lands in the same position")
#expect(droppedCards.map(\.order) == movedCards.map(\.order),
"index \(index): and takes the same ranks")
#expect(droppedCards[index].order == movedCards[index].order)
#expect(fileStore.banners.oneShots.isEmpty)
#expect(cardStore.banners.oneShots.isEmpty)
}
}
@Test("An empty lane takes the drop at its only position")
func emptyLane() throws {
let fixture = try makeBoard()
@@ -500,6 +536,18 @@ struct FinderDropFolderTests {
@Suite("Finder drop ▸ what the drag is carrying")
struct FinderDropPayloadTests {
/// A provider announcing one declared type and nothing else the hover read's whole input.
@MainActor
private func provider(_ type: UTType) -> NSItemProvider {
let provider = NSItemProvider()
provider.registerDataRepresentation(forTypeIdentifier: type.identifier, visibility: .all) {
completion in
completion(Data(), nil)
return nil
}
return provider
}
@Test("Conformance to public.directory is the test — folders and packages alike")
func directoryTypes() {
#expect(FinderDrop.isDirectory(typeIdentifiers: ["public.folder", "public.file-url"]))
@@ -526,16 +574,6 @@ struct FinderDropPayloadTests {
@MainActor
@Test("The importable count is the file count — folders are not counted, so they draw no shadow")
func importableCountCountsFilesOnly() {
func provider(_ type: UTType) -> NSItemProvider {
let provider = NSItemProvider()
provider.registerDataRepresentation(forTypeIdentifier: type.identifier, visibility: .all) {
completion in
completion(Data(), nil)
return nil
}
return provider
}
#expect(FinderDrop.importableCount([provider(.png), provider(.folder)]) == 1)
#expect(FinderDrop.importableCount([provider(.png), provider(.plainText)]) == 2)
// Zero is the refusal itself: `acceptsFileDrop` is false, so the drag never engages.
@@ -543,6 +581,30 @@ struct FinderDropPayloadTests {
#expect(FinderDrop.importableCount([]) == 0)
}
/// **"One nominal-height shadow per incoming file"** (04-interactions.md Drag and drop, settled
/// 2026-07-28 the multi-drag precedent), **"and when macOS withholds item counts during hover
/// the count floors at one shadow, the commit unaffected"**.
@MainActor
@Test("The shadow run is one shadow per importable file, floored at one")
func shadowCountIsTheImportableCountFlooredAtOne() {
#expect(FinderDrop.shadowCount([provider(.png)]) == 1)
#expect(FinderDrop.shadowCount([provider(.png), provider(.plainText)]) == 2)
#expect(FinderDrop.shadowCount([provider(.png), provider(.plainText), provider(.pdf)]) == 3)
// A mixed drag's shadows agree with what will actually be minted: files only.
#expect(FinderDrop.shadowCount([provider(.png), provider(.folder), provider(.folder)]) == 1)
// The floor. A drag whose providers the system will not count still opens a slot the user can
// aim at; the write counts resolved URLs, so a lone shadow standing in for a whole drag costs
// the drop nothing.
#expect(FinderDrop.shadowCount([]) == 1)
#expect(FinderDrop.shadowCount([provider(.folder), provider(.folder)]) == 1)
// Everywhere the count is real, it is exactly the importable count.
for payload in [[provider(.png)], [provider(.png), provider(.folder), provider(.plainText)]] {
#expect(FinderDrop.shadowCount(payload) == FinderDrop.importableCount(payload))
}
}
@Test("The drop reads the filesystem, which is the authority a declared type is not")
func filesystemIsAuthoritative() throws {
let sources = try DropSources()