mana-swift-ui/Sources/ManaComponentsUI/ManaEmptyStateView.swift
Till JS 5d30d5a840 feat(ManaComponentsUI): ManaEmptyStateView + PlatformClipboard
- 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>
2026-06-03 13:33:39 +02:00

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)
}
}