Build the drop-slot model and the drop commits — drag & drop, first half
The pathfinder's drag-reorder model, ported and generalized (DRAG-REORDER.md travels with it, rewritten for lanes, the interior masonry, multi-drag, cross-board sessions, the re-grounding trio, and the committed-overlay hold): - DropSlotMath — resting-layout zones from analytic lane arithmetic and the pure masonry placement (MasonryLayout now lays out through the same MasonryPlacement the drag reads, so geometry cannot drift), span-capped triggers sized to the dragged run's future footprint, hysteresis holds with the fresh-entry fallback, boundary ties, own-slot no-ops; nil means hold. - DragAutoScrollMath — the activation bands and velocity ramp, pure. - The drop commits, one performWrite bracket each: moveCards/copyCards within a board (insertion ranks touch only the dragged cards; renumber fallback); receiveCards/receiveLanes/receiveRestoredCards on the destination store for cross-board copy and ⌘-move with the import-boundary remint, lane copies stripping tombstoned cards while moves carry them; restoreByDrag is now positional, writing order only when the drop names a new one. Gestures, sessions, previews, and delegates are the second half. 773 unit tests (87 new since the keyboard grammar). Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -369,13 +369,15 @@ struct TrashDragRestoreTests {
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
let original = try fixture.indexText("\(Ident.lane1)/\(Ident.card2)")
|
||||
|
||||
store.restoreByDrag(cardID: card2, intoLane: lane1)
|
||||
store.restoreByDrag(cardID: card2, intoLane: lane1, at: 1)
|
||||
|
||||
let after = try fixture.indexText("\(Ident.lane1)/\(Ident.card2)")
|
||||
#expect(fixture.exists("\(Ident.lane1)/\(Ident.card2)"))
|
||||
#expect(!after.contains("deleted"))
|
||||
// The `order` is untouched, so the card returns where it was rather than at the bottom —
|
||||
// the position-perfect restore a pure-view trash makes possible.
|
||||
// Dropped at index 1 — past lane one's single live card, which is exactly where the card's
|
||||
// recorded 2048 already puts it. The rank the drop names and the rank on disk agree, so no
|
||||
// `order` is written at all: the `order` line comes through byte-for-byte and the card
|
||||
// returns where it was, the position-perfect restore a pure-view trash makes possible.
|
||||
#expect(try FrontmatterDocument.parse(after).order == .valid(2048))
|
||||
#expect(untouchedLines(after) == untouchedLines(original))
|
||||
}
|
||||
@@ -386,14 +388,14 @@ struct TrashDragRestoreTests {
|
||||
defer { fixture.tearDown() }
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
|
||||
store.restoreByDrag(cardID: card2, intoLane: lane2)
|
||||
store.restoreByDrag(cardID: card2, intoLane: lane2, at: 1)
|
||||
|
||||
#expect(!fixture.exists("\(Ident.lane1)/\(Ident.card2)"))
|
||||
#expect(fixture.exists("\(Ident.lane2)/\(Ident.card2)"))
|
||||
let after = try fixture.indexText("\(Ident.lane2)/\(Ident.card2)")
|
||||
#expect(!after.contains("deleted"))
|
||||
// m5-drag: the positional drop replaces this append. Lane two's one visible card is at
|
||||
// 1024, so the arrival lands at 2048 — the Writer's own append over visible siblings.
|
||||
// Dropped at index 1 — lane two's one visible card is at 1024, so the drop's own rank is
|
||||
// the append 2048, carried by the move rather than left to the Writer to compute.
|
||||
#expect(try FrontmatterDocument.parse(after).order == .valid(2048))
|
||||
|
||||
let model = try BoardLoader.load(boardRoot: fixture.root).model
|
||||
@@ -413,13 +415,13 @@ struct TrashDragRestoreTests {
|
||||
|
||||
// A tombstoned destination lane is never a drop target (04 ▸ Drag and drop: "a card is
|
||||
// never filed under a `deleted:` parent").
|
||||
store.restoreByDrag(cardID: card2, intoLane: lane3)
|
||||
store.restoreByDrag(cardID: card2, intoLane: lane3, at: 0)
|
||||
// A lane that is not on the board at all.
|
||||
store.restoreByDrag(cardID: card2, intoLane: ItemID(rawValue: Ident.indexless))
|
||||
store.restoreByDrag(cardID: card2, intoLane: ItemID(rawValue: Ident.indexless), at: 0)
|
||||
// A card that is not a trash row: live, and — for `card4` — hidden by its lane rather than
|
||||
// by its own flag, so it has no row to drag in the first place.
|
||||
store.restoreByDrag(cardID: card1, intoLane: lane2)
|
||||
store.restoreByDrag(cardID: card4, intoLane: lane1)
|
||||
store.restoreByDrag(cardID: card1, intoLane: lane2, at: 0)
|
||||
store.restoreByDrag(cardID: card4, intoLane: lane1, at: 0)
|
||||
|
||||
#expect(try stat(fixture, "\(Ident.lane1)/\(Ident.card2)").modified == trashedCard.modified)
|
||||
#expect(try stat(fixture, "\(Ident.lane1)/\(Ident.card1)").modified == liveCard.modified)
|
||||
@@ -434,7 +436,7 @@ struct TrashDragRestoreTests {
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
store.enterVanishedRootLock()
|
||||
|
||||
store.restoreByDrag(cardID: card2, intoLane: lane2)
|
||||
store.restoreByDrag(cardID: card2, intoLane: lane2, at: 1)
|
||||
|
||||
#expect(fixture.exists("\(Ident.lane1)/\(Ident.card2)"))
|
||||
#expect(try fixture.indexText("\(Ident.lane1)/\(Ident.card2)").contains("deleted:"))
|
||||
|
||||
Reference in New Issue
Block a user