Files

117 lines
3.7 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import SwiftUI
public nonisolated struct DropTargetPresentation: Equatable, Sendable {
public nonisolated enum Tone: Equatable, Sendable {
case accent
case unavailable
}
public let title: String
public let detail: String
public let systemImage: String
public var tone: Tone
public init(
title: String,
detail: String,
systemImage: String,
tone: Tone = .accent
) {
self.title = title
self.detail = detail
self.systemImage = systemImage
self.tone = tone
}
}
/// One unmistakable drop affordance for every full-surface destination.
///
/// The content under it remains visible enough to retain place, while the
/// moving dashed border and the compact glass card make the pending action
/// legible over a terminal, a file list, or a transcript.
public struct DropTargetOverlay: View {
public let presentation: DropTargetPresentation
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var animatesSymbol = false
private let cornerRadius: CGFloat = 10
private var tint: Color {
presentation.tone == .unavailable ? .orange : .accentColor
}
public init(presentation: DropTargetPresentation) {
self.presentation = presentation
}
public var body: some View {
ZStack {
tint.opacity(0.09)
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(
tint,
style: StrokeStyle(lineWidth: 2, dash: [7, 5])
)
.padding(8)
VStack(spacing: 6) {
Image(systemName: presentation.systemImage)
.font(.system(size: 24, weight: .semibold))
.foregroundStyle(tint)
.symbolEffect(
.bounce.up,
options: reduceMotion ? .nonRepeating : .repeating.speed(0.55),
value: animatesSymbol
)
Text(presentation.title)
.font(.headline)
Text(presentation.detail)
.font(.caption.monospaced())
.foregroundStyle(.secondary)
.lineLimit(2)
.truncationMode(.middle)
}
.multilineTextAlignment(.center)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.modifier(DropTargetCardSurface(cornerRadius: cornerRadius))
}
.allowsHitTesting(false)
.accessibilityElement(children: .combine)
.transition(
reduceMotion
? .opacity
: .opacity.combined(with: .scale(scale: 0.96))
)
.onAppear { animatesSymbol = true }
}
}
private struct DropTargetCardSurface: ViewModifier {
let cornerRadius: CGFloat
@ViewBuilder
func body(content: Content) -> some View {
let shape = RoundedRectangle(cornerRadius: cornerRadius)
if #available(iOS 26.0, macOS 26.0, *) {
content
.glassEffect(.regular, in: shape)
.overlay {
shape.strokeBorder(Color.primary.opacity(0.10), lineWidth: 1)
}
.shadow(color: .black.opacity(0.16), radius: 12, y: 5)
} else {
content
.background(.regularMaterial, in: shape)
.overlay {
shape.strokeBorder(Color.primary.opacity(0.10), lineWidth: 1)
}
.shadow(color: .black.opacity(0.16), radius: 12, y: 5)
}
}
}