72 lines
2.6 KiB
Swift
72 lines
2.6 KiB
Swift
import SwiftUI
|
|
|
|
struct EmptyStateView: View {
|
|
let title: LocalizedStringKey
|
|
let systemImage: String
|
|
let description: LocalizedStringKey
|
|
var action: (() -> Void)?
|
|
var actionLabel: LocalizedStringKey?
|
|
/// Halo/icon tint — positive states pass a color (e.g. green for "All
|
|
/// Clean"); neutral states keep the secondary look.
|
|
var tint: Color?
|
|
|
|
@State private var floating = false
|
|
@State private var popped = false
|
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
|
|
init(_ title: LocalizedStringKey, systemImage: String, description: LocalizedStringKey,
|
|
action: (() -> Void)? = nil, actionLabel: LocalizedStringKey? = nil, tint: Color? = nil) {
|
|
self.title = title
|
|
self.systemImage = systemImage
|
|
self.description = description
|
|
self.action = action
|
|
self.actionLabel = actionLabel
|
|
self.tint = tint
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 12) {
|
|
Spacer()
|
|
ZStack {
|
|
Circle()
|
|
.fill((tint ?? Color.secondary).opacity(0.10))
|
|
.frame(width: 96, height: 96)
|
|
Image(systemName: systemImage)
|
|
.font(.system(size: 40))
|
|
.foregroundStyle(tint ?? Color.secondary)
|
|
}
|
|
// One-shot entrance pop, then a gentle idle float. Both skipped
|
|
// under Reduce Motion.
|
|
.scaleEffect(popped || reduceMotion ? 1 : 0.8)
|
|
.offset(y: reduceMotion ? 0 : (floating ? -5 : 5))
|
|
.onAppear {
|
|
guard !reduceMotion else { return }
|
|
withAnimation(.spring(response: 0.4, dampingFraction: 0.6)) {
|
|
popped = true
|
|
}
|
|
withAnimation(.easeInOut(duration: 2.4).repeatForever(autoreverses: true)) {
|
|
floating = true
|
|
}
|
|
}
|
|
|
|
Text(title)
|
|
.font(.title3.bold())
|
|
.staggered(0, baseDelay: 0.08)
|
|
Text(description)
|
|
.font(.callout)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.frame(maxWidth: 300)
|
|
.staggered(1, baseDelay: 0.08)
|
|
if let action, let label = actionLabel {
|
|
Button(action: action) { Text(label) }
|
|
.buttonStyle(.borderedProminent)
|
|
.padding(.top, 4)
|
|
.staggered(2, baseDelay: 0.08)
|
|
}
|
|
Spacer()
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|