DragReplicaStyle spells the pathfinder's constant ambient shadow once (black 16%, radius 2.5, y 1.5) and all three replica faces — card, lane, trashed lane — wear it via one modifier; the ghost fans compose from the same faces so every copy inherits it, dimmed by its own opacity. 03 § Motion's "pickup lift (scale + shadow)" supplies the scale from the system session but no shadow on the preview image — the replica is the only place it can come from. BoardMetrics.replicaPadding was already written to keep exactly this shadow from clipping and was simply unused until now. Drag-perf card 24397310. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
101 lines
4.8 KiB
Swift
101 lines
4.8 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 — the lane's plate radius or the card's, both
|
|
/// font-derived (`BoardMetrics`) and both supplied by the caller, which is the one that knows
|
|
/// which surface this shadow is standing in for.
|
|
let cornerRadius: CGFloat
|
|
|
|
/// A drop proposal (dashed) or a resting footprint (plain).
|
|
var dashed: Bool = true
|
|
|
|
/// Increase Contrast and Reduce Transparency, for the outline and the fill below
|
|
/// (10-accessibility.md; `Accommodations`). A shadow's whole job is to say "here", and both
|
|
/// halves of how it says so are alpha-based by default.
|
|
@Environment(\.colorSchemeContrast) private var contrast
|
|
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
|
|
|
|
var body: some View {
|
|
RoundedRectangle(cornerRadius: cornerRadius)
|
|
.fill(Accommodations.dragShadowWash(reduceTransparency: reduceTransparency).style)
|
|
.overlay {
|
|
if dashed {
|
|
RoundedRectangle(cornerRadius: cornerRadius)
|
|
.strokeBorder(
|
|
Color.accentColor.opacity(Accommodations.accentOpacity(0.55, contrast: contrast)),
|
|
style: StrokeStyle(
|
|
lineWidth: Accommodations.borderWidth(1.5, contrast: contrast),
|
|
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
|
|
|
|
/// The badge is a disc around a numeral, so every figure in it follows the numeral's font
|
|
/// (`BoardMetrics`, 10-accessibility.md's full-relative-scaling rule) — at the standard body size
|
|
/// they are the 6, 3 and 10 points it has always drawn.
|
|
private var pointSize: CGFloat { BoardMetrics.bodyPointSize }
|
|
|
|
var body: some View {
|
|
if count > 1 {
|
|
Text("\(count)")
|
|
.font(.caption2.bold())
|
|
.monospacedDigit()
|
|
.foregroundStyle(.white)
|
|
.padding(.horizontal, BoardMetrics.em(0.45, bodyPointSize: pointSize))
|
|
.padding(.vertical, BoardMetrics.em(0.25, bodyPointSize: pointSize))
|
|
.background(Circle().fill(Color.accentColor))
|
|
.offset(
|
|
x: BoardMetrics.em(0.75, bodyPointSize: pointSize),
|
|
y: -BoardMetrics.em(0.75, bodyPointSize: pointSize)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The pickup lift's shadow figures — 03-board-ui.md § Motion's "the pickup lift (scale + shadow as
|
|
/// it detaches from the card)": the system drag session supplies the scale and the tracking, but
|
|
/// paints no shadow onto its preview image, so the shadow can only come from the replica view
|
|
/// itself. Values are the pathfinder's own constant ambient shadow, reused verbatim rather than
|
|
/// re-derived (`~/Documents/indie/Kanban` CardView.swift:623) — its drags read subtly raised, and
|
|
/// that read is the point.
|
|
enum DragReplicaStyle {
|
|
static let shadowColor = Color.black.opacity(0.16)
|
|
static let shadowRadius: CGFloat = 2.5
|
|
static let shadowY: CGFloat = 1.5
|
|
}
|
|
|
|
extension View {
|
|
/// The pickup lift's shadow (`DragReplicaStyle`), for a drag replica's face — shared by the
|
|
/// card, lane, and trashed-lane replicas so the figures are spelled once.
|
|
func dragReplicaShadow() -> some View {
|
|
shadow(color: DragReplicaStyle.shadowColor, radius: DragReplicaStyle.shadowRadius, y: DragReplicaStyle.shadowY)
|
|
}
|
|
}
|