- ManaEmptyStateView: geteilter Empty-/Zustands-Render (Symbol mit Theme-Akzent-Glow, Titel/Text, optionaler CTA). Farben aus @Environment(\.manaTheme). Blaupause: memoros EmptyStateView. - PlatformClipboard: plattformübergreifendes Kopieren (UIPasteboard/ NSPasteboard). Liegt hier statt in ManaCore, weil ManaCore UI-Framework-frei bleiben soll (Invariante 2). CHANGELOG-Eintrag bleibt vorerst aus diesem Commit (CHANGELOG.md im Repo vermischt sich mit anderer uncommitteter Arbeit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
3.7 KiB
Swift
110 lines
3.7 KiB
Swift
import ManaTokens
|
|
import SwiftUI
|
|
|
|
/// Geteilter Empty-/Zustands-Render für alle nativen mana-Apps. Geht über
|
|
/// das system-graue `ContentUnavailableView` hinaus: Vereins-Akzent hinter
|
|
/// dem Symbol (weicher Radial-Glow) + optionaler CTA-Button.
|
|
///
|
|
/// Die Akzentfarbe kommt standardmäßig aus der aktiven Theme-Variant
|
|
/// (`@Environment(\.manaTheme)`); per `tint` überschreibbar (z. B. eine
|
|
/// gedeckte Sekundärfarbe für Fehler-/Such-Leerzustände).
|
|
///
|
|
/// ```swift
|
|
/// ManaEmptyStateView(
|
|
/// symbol: "waveform.circle.fill",
|
|
/// title: "Noch keine Aufnahmen",
|
|
/// description: "Tippe auf den roten Punkt, um zu starten.",
|
|
/// cta: .init(label: "Aufnahme starten", systemImage: "mic.fill") { … }
|
|
/// )
|
|
/// ```
|
|
public struct ManaEmptyStateView: View {
|
|
@Environment(\.manaTheme) private var theme
|
|
|
|
/// Optionaler Aufruf-zur-Aktion unter dem Text (prominenter Button).
|
|
public struct CTA {
|
|
public let label: String
|
|
public var systemImage: String?
|
|
public let action: () -> Void
|
|
|
|
public init(label: String, systemImage: String? = nil, action: @escaping () -> Void) {
|
|
self.label = label
|
|
self.systemImage = systemImage
|
|
self.action = action
|
|
}
|
|
}
|
|
|
|
private let symbol: String
|
|
private let title: String
|
|
private let description: String?
|
|
private let cta: CTA?
|
|
private let tint: Color?
|
|
|
|
/// - Parameters:
|
|
/// - symbol: SF-Symbol für die Illustration.
|
|
/// - title: Überschrift.
|
|
/// - description: optionaler erklärender Satz.
|
|
/// - tint: optionale Akzentfarbe. `nil` → Theme-Primary.
|
|
/// - cta: optionaler Aktions-Button.
|
|
public init(
|
|
symbol: String,
|
|
title: String,
|
|
description: String? = nil,
|
|
tint: Color? = nil,
|
|
cta: CTA? = nil
|
|
) {
|
|
self.symbol = symbol
|
|
self.title = title
|
|
self.description = description
|
|
self.tint = tint
|
|
self.cta = cta
|
|
}
|
|
|
|
public var body: some View {
|
|
let accent = tint ?? theme.colors.primary
|
|
VStack(spacing: 16) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(accent.opacity(0.12))
|
|
.frame(width: 110, height: 110)
|
|
.blur(radius: 18)
|
|
Image(systemName: symbol)
|
|
.font(.system(size: 56, weight: .regular))
|
|
.symbolRenderingMode(.hierarchical)
|
|
.foregroundStyle(accent)
|
|
}
|
|
.padding(.bottom, 4)
|
|
|
|
VStack(spacing: 6) {
|
|
Text(title)
|
|
.font(.title3.weight(.semibold))
|
|
.foregroundStyle(theme.colors.foreground)
|
|
.multilineTextAlignment(.center)
|
|
if let description {
|
|
Text(description)
|
|
.font(.callout)
|
|
.foregroundStyle(theme.colors.mutedForeground)
|
|
.multilineTextAlignment(.center)
|
|
.frame(maxWidth: 320)
|
|
}
|
|
}
|
|
|
|
if let cta {
|
|
Button(action: cta.action) {
|
|
if let image = cta.systemImage {
|
|
Label(cta.label, systemImage: image)
|
|
.font(.callout.weight(.medium))
|
|
} else {
|
|
Text(cta.label)
|
|
.font(.callout.weight(.medium))
|
|
}
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.controlSize(.large)
|
|
.tint(accent)
|
|
.padding(.top, 6)
|
|
}
|
|
}
|
|
.padding(24)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|