Realign code with the 2026-07-31 rulings
The trash sorts by modified descending — the arrival rank mint retires (Ranks.isOrderedForTrash one comparator, loader + merged order agree; the legacy deleted: migration stamps modified from the tombstone timestamp where parseable; delete undo steps validate existence-only; agent guide v8). Trash selection goes kind-blind — ranges, marquee, Select All, and the successor walk sweep both kinds; the guard moves to the exits (mixed-payload drop refusal, copy/cut validation). The copy stamping preflight widens back to comment depth (load-scoped posture — the board always loads, the gesture refuses whole). Fixes a latent no-op: trashed-lane drag restore never fired (DragSession.beginLanes hard-coded the board container). 2403 tests in 413 suites green. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
@@ -60,12 +60,11 @@ enum TrashDrop {
|
||||
|
||||
/// The row the shadow takes, always: **the topmost**.
|
||||
///
|
||||
/// Not arbitrary, and the ranks are what make it honest: "every trash arrival mints a rank
|
||||
/// above the current top" (04-interactions.md ▸ The trash), so a fresh delete genuinely lands on
|
||||
/// top. The drop
|
||||
/// therefore still lands exactly where the shadow shows — the one positional promise every other
|
||||
/// drop in this app makes — while being the only proposal on the board the *pointer* does not
|
||||
/// choose.
|
||||
/// Not arbitrary, and the stamp is what makes it honest: "every trash arrival stamps `modified`
|
||||
/// and the trash sorts newest-first by that stamp" (04-interactions.md ▸ The trash, re-ruled
|
||||
/// 2026-07-31), so a fresh delete genuinely lands on top. The drop therefore still lands exactly
|
||||
/// where the shadow shows — the one positional promise every other drop in this app makes —
|
||||
/// while being the only proposal on the board the *pointer* does not choose.
|
||||
static let landingIndex = 0
|
||||
|
||||
/// Whether the shown trash takes this session — the whole of the gate, and every clause is a
|
||||
@@ -284,6 +283,18 @@ final class DragSession {
|
||||
/// and that is the whole of what makes it a restore (04-interactions.md ▸ The trash).
|
||||
private(set) var container: ItemContainer = .board
|
||||
|
||||
/// **Whether the selection this drag was picked up from spanned both kinds** — only ever true in
|
||||
/// the trash, whose selection went kind-blind (04-interactions.md ▸ The trash, ruled 2026-07-31).
|
||||
///
|
||||
/// A session carries items of exactly one `kind` — the pasteboard type is per-kind and a
|
||||
/// `DragPayload` names one — so a mixed selection dragged from a trash row starts a session over
|
||||
/// *its* kind and silently leaves the other behind. That silence is what this flag exists to
|
||||
/// prevent: "pickup is allowed — the selection is legal — but every out-of-trash drop target
|
||||
/// refuses the mixed payload, and the release surfaces a notice explaining the rule". The refusal
|
||||
/// is at the commit (`BoardDropContext.commitDrop`), where every drop out of the trash funnels,
|
||||
/// rather than at hover — 04 puts the explanation at the release.
|
||||
private(set) var mixesKinds = false
|
||||
|
||||
/// The dragged items in **flatten order** — the order they will land in.
|
||||
private(set) var members: [ItemID] = []
|
||||
|
||||
@@ -496,16 +507,33 @@ final class DragSession {
|
||||
folders: [URL],
|
||||
heights: [CGFloat],
|
||||
container: ItemContainer,
|
||||
source: BoardStore
|
||||
source: BoardStore,
|
||||
mixesKinds: Bool = false
|
||||
) {
|
||||
begin(kind: .cards, members: members, folders: folders, container: container, source: source)
|
||||
begin(
|
||||
kind: .cards, members: members, folders: folders,
|
||||
container: container, source: source, mixesKinds: mixesKinds
|
||||
)
|
||||
cardHeights = heights
|
||||
laneUnits = []
|
||||
}
|
||||
|
||||
/// Begins a lane session.
|
||||
func beginLanes(_ members: [ItemID], folders: [URL], units: [Int], source: BoardStore) {
|
||||
begin(kind: .lanes, members: members, folders: folders, container: .board, source: source)
|
||||
///
|
||||
/// `container` is a parameter because a **trashed lane row**'s drag is a `.lanes` session in
|
||||
/// `.trash` — the restore at the lane level (04-interactions.md ▸ The trash ▸ Drag-to-restore).
|
||||
func beginLanes(
|
||||
_ members: [ItemID],
|
||||
folders: [URL],
|
||||
units: [Int],
|
||||
container: ItemContainer = .board,
|
||||
source: BoardStore,
|
||||
mixesKinds: Bool = false
|
||||
) {
|
||||
begin(
|
||||
kind: .lanes, members: members, folders: folders,
|
||||
container: container, source: source, mixesKinds: mixesKinds
|
||||
)
|
||||
laneUnits = units
|
||||
cardHeights = []
|
||||
}
|
||||
@@ -515,7 +543,8 @@ final class DragSession {
|
||||
members: [ItemID],
|
||||
folders: [URL],
|
||||
container: ItemContainer,
|
||||
source: BoardStore
|
||||
source: BoardStore,
|
||||
mixesKinds: Bool
|
||||
) {
|
||||
endHold()
|
||||
self.kind = kind
|
||||
@@ -523,6 +552,7 @@ final class DragSession {
|
||||
self.memberSet = Set(members)
|
||||
self.folders = folders
|
||||
self.container = container
|
||||
self.mixesKinds = mixesKinds
|
||||
self.sourceStore = source
|
||||
self.sourceRoot = source.rootURL
|
||||
self.proposal = nil
|
||||
|
||||
Reference in New Issue
Block a user