The second half: system drag sessions over phase 1's model, per DRAG-REORDER.md and 04-interactions.md § Drag & drop. - Card faces, lane headers, and trash rows drag as NSItemProvider sessions (two exported UTTypes, JSON payload in flatten order, plain-text titles as the secondary representation) — replacing m4's custom lane-reorder gesture and trash drag-out wholesale; the app-wide DragSession carries the members, the frozen dragged sizes, the live proposal, and the effective operation. - Three drop delegates (lane masonry, strip, window fallback), each accepting both types and routing internally per the single-target-dispatch rule; the cursor is the physical mouse converted to strip space; proposals come from DropSlotMath with hysteresis threaded through, and the lane-strip proposal clamps in front of the shown trash. - Locality picks the default — move within a board, copy across, the badge tracking live; ⌥ forces copy (ignored on within-board lane drags), ⌘ forces move; trash rows restore within their board (positional), copy out across boards by default, ⌘ forcing the true restore-move. - N contiguous shadows with reflow keyed on the proposal; the committed-overlay hold renders the dropped arrangement until the reload echo lands (1.5 s dissolution deadline for refused writes); the re-grounding trio: geometry re-derives per render, proposals re-validate by liveness at release, an emptied drag cancels itself. - Edge autoscroll (ticking driver over DragAutoScrollMath, re-targeting per step), the mouse-up-gated late-event cleanup, and the polling watchdog — the pathfinder's lifecycle traps, ported. - Store: moveLanes and multi-card restoreByDrag join the one-bracket drop commits. 784 unit tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
62 lines
2.4 KiB
Swift
62 lines
2.4 KiB
Swift
import SwiftUI
|
|
|
|
/// The **shadow placeholder** — the outline occupying an item's proposed landing spot
|
|
/// (DRAG-REORDER.md § The pieces).
|
|
///
|
|
/// One shape for both the drag's shadows and the lane resize's resting footprint, because they are
|
|
/// the same thing said twice: "here is the space this item occupies in the layout". They differ only
|
|
/// in whether the space is a *proposal* — the drag's is dashed, because a dashed outline is what
|
|
/// 03-board-ui.md's placeholder vocabulary asks for and what makes "the drop lands exactly here"
|
|
/// legible; the resize's is a plain fill, because the lane is already there.
|
|
///
|
|
/// **Hit-transparent, always.** The strip's own drop target has to stay live beneath the shadows
|
|
/// (DRAG-REORDER.md § Single-target dispatch: "shadow placeholders are hit-transparent, so the strip
|
|
/// target stays live beneath them"), and a shadow that swallowed the release would strand the drop.
|
|
struct DragShadow: View {
|
|
|
|
/// Matched to the surface it stands in for — a lane's plate is 10, a card's is 8.
|
|
var cornerRadius: CGFloat = 10
|
|
|
|
/// A drop proposal (dashed) or a resting footprint (plain).
|
|
var dashed: Bool = true
|
|
|
|
var body: some View {
|
|
RoundedRectangle(cornerRadius: cornerRadius)
|
|
.fill(.quaternary.opacity(0.5))
|
|
.overlay {
|
|
if dashed {
|
|
RoundedRectangle(cornerRadius: cornerRadius)
|
|
.strokeBorder(
|
|
Color.accentColor.opacity(0.55),
|
|
style: StrokeStyle(lineWidth: 1.5, dash: [6])
|
|
)
|
|
}
|
|
}
|
|
.allowsHitTesting(false)
|
|
.accessibilityHidden(true)
|
|
}
|
|
}
|
|
|
|
/// The count badge a multi-drag's replica wears — Finder's own affordance for "this many are coming
|
|
/// with me" (04-interactions.md ▸ Drag and drop: "dragging any member of a multi-selection drags the
|
|
/// whole selection").
|
|
///
|
|
/// Draws nothing for a single item, so every replica can carry it unconditionally.
|
|
struct DragCountBadge: View {
|
|
|
|
let count: Int
|
|
|
|
var body: some View {
|
|
if count > 1 {
|
|
Text("\(count)")
|
|
.font(.caption2.bold())
|
|
.monospacedDigit()
|
|
.foregroundStyle(.white)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 3)
|
|
.background(Circle().fill(Color.accentColor))
|
|
.offset(x: 10, y: -10)
|
|
}
|
|
}
|
|
}
|