Files
lanework/Kanban/UI/Board/DragShadow.swift
T
rzen 3f4125e324 Render the dropped card at release, and pin the hold's timeout
03's sharpened settle rule: rendering the arrangement means rendering
the card — at release the shadow swaps for the dropped card(s) drawn in
place immediately, the appear never waiting for the echo reload. The
committed hold now carries the landing (ids, payload titles, operation)
and surfaces read one DropLanding seam: within-board moves draw the
real faces at their proposed slots under the arriving card's own key,
so the echo is an invisible content swap; cross-board card arrivals
draw payload-titled faces keyed positionally, so the echo reads as an
ordinary arrival. Cross-board lane arrivals deliberately keep their
shadow until the echo — a lane's face is a whole column with no honest
payload equivalent. The 1500 ms failed-write timeout is now seamed
(injectable duration, extracted expire) and pinned by tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-28 07:51:05 -04:00

68 lines
2.9 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.
///
/// **A drag's shadow lives exactly as long as the drag does.** The moment the mouse comes up the
/// slot it was holding draws the dropped card itself (`LaneView`'s `runSlots`, `DroppedCardFace`) —
/// "a lingering shadow over a hidden card is the hold failing its one job" (03-board-ui.md § Motion,
/// sharpened 2026-07-28). The one shadow that outlives a release is the cross-board *lane* arrival's,
/// which has no column to draw until the echo lands (`BoardView.runSlots`).
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)
}
}
}