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
+27 -6
View File
@@ -109,9 +109,18 @@ struct FileDropTarget: Equatable, Sendable {
enum Landing: Equatable, Sendable {
/// **Onto a card**: the files copy into that card's `attachments/`. A card under the cursor
/// always wins over the lane behind it attach beats create, anywhere on the card's bounds.
/// This is the one file landing that **highlights**, and only because it has no shadow to
/// show instead (04-interactions.md Drag and drop, settled 2026-07-28).
case attach(cardID: ItemID)
/// **Onto lane empty space**: one card per file, landing at this position in the lane's
/// **Onto the lane's card grid**: one card per file, landing at this position in the lane's
/// logical card order.
///
/// **Positional, everywhere the grid reaches** (04-interactions.md Drag and drop, settled
/// 2026-07-28): "created cards land at the drop position resolved through the same
/// card-grid zones an ordinary card drag uses, shadow included", and a release on the lane
/// **header** resolves to index 0, the topmost position. Append-at-bottom is the creation
/// trio's rule (N, Return, a double click), never the drop's. `FileDropZones.landing` is
/// where the index comes from.
case create(laneID: ItemID, index: Int)
}
@@ -121,12 +130,15 @@ struct FileDropTarget: Equatable, Sendable {
var landing: Landing
/// How many files ride along the create path's shadow run length, one shadow per card that
/// will land. Read off the session's item providers at hover time, floored at one.
/// How many files ride along **one nominal-height shadow per incoming file** on the create path
/// (04-interactions.md Drag and drop, settled 2026-07-28: the multi-drag precedent), one shadow
/// per card that will land. Read off the session's item providers at hover time
/// (`FinderDrop.shadowCount`), **floored at one** for the drag whose count macOS withholds the
/// commit is unaffected either way, since the write counts resolved URLs, not providers.
///
/// **Folders are not counted** (04-interactions.md Drag and drop: "a mixed drag proposes for
/// its files only"): `FinderDrop.importableCount` reads the providers' declared types, so a
/// mixed drag draws shadows for its files alone and a folders-only drag never proposes at all.
/// **Folders are not counted** (same bullet: "a mixed drag proposes for its files only"):
/// `FinderDrop.importableCount` reads the providers' declared types, so a mixed drag draws
/// shadows for its files alone and a folders-only drag never proposes at all.
var fileCount: Int
}
@@ -568,6 +580,15 @@ final class DragSession {
/// The shadow run a file drop would open in `laneID` on this board where the created cards
/// land and how many there are or `nil` when the proposal is elsewhere.
///
/// **This run is the create path's whole feedback** (04-interactions.md Drag and drop, settled
/// 2026-07-28): there is no lane-level highlight to go with it, deliberately each target gets
/// one clear signal, and `fileAttachTarget` above is the highlight precisely because a card
/// target has no shadow.
///
/// The floor restates `FinderDrop.shadowCount`'s, at the render end rather than in place of it: a
/// proposal that somehow carried a zero would otherwise open a run of no shadows at all, which is
/// a landing spot the user cannot see.
func fileLaneProposal(onBoardRooted root: URL, laneID: ItemID) -> (index: Int, count: Int)? {
guard let fileTarget,
DragLocality.isSameBoard(fileTarget.boardRoot, root),