Files
lanework/Kanban/UI/Board/DragShadow.swift
T
rzen 90cf82d740 Implement drag & drop with the locality model
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
2026-07-27 20:58:26 -04:00

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)
}
}
}