import SwiftUI enum AppUI { static let accent = Color(red: 0.91, green: 0.48, blue: 0.12) static let cardCornerRadius: CGFloat = 14 static let compactCornerRadius: CGFloat = 8 } struct LocalStatusLabel: View { let databaseName: String? var body: some View { HStack(spacing: 7) { Circle() .fill(.green) .frame(width: 7, height: 7) Text("Lokal · nicht synchronisiert") .font(.caption) .foregroundStyle(.secondary) if let databaseName { Text("·") .foregroundStyle(.tertiary) Text(databaseName) .font(.caption.monospaced()) .foregroundStyle(.tertiary) .lineLimit(1) } } .accessibilityElement(children: .combine) .accessibilityLabel("Lokale Datenbank, keine Synchronisierung") } } struct StatusChip: View { let title: String var systemImage: String? = nil var tint: Color = .secondary var body: some View { HStack(spacing: 5) { if let systemImage { Image(systemName: systemImage) } Text(title) .lineLimit(1) } .font(.caption.weight(.medium)) .foregroundStyle(tint) .padding(.horizontal, 8) .padding(.vertical, 4) .background(tint.opacity(0.11), in: Capsule()) } } struct ErrorBanner: View { let error: AppErrorPresentation let dismiss: () -> Void let copyDiagnostic: () -> Void var body: some View { HStack(alignment: .top, spacing: 12) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.red) .padding(.top, 2) VStack(alignment: .leading, spacing: 3) { Text(error.title) .font(.subheadline.weight(.semibold)) Text(error.message) .font(.caption) .foregroundStyle(.secondary) .textSelection(.enabled) } Spacer(minLength: 16) Button("Diagnose kopieren", action: copyDiagnostic) .buttonStyle(.borderless) .font(.caption) Button(action: dismiss) { Image(systemName: "xmark") } .buttonStyle(.borderless) .accessibilityLabel("Fehler ausblenden") } .padding(12) .background(.red.opacity(0.08)) .overlay(alignment: .bottom) { Divider().overlay(.red.opacity(0.2)) } } } struct SuccessToast: View { let message: String var body: some View { Label(message, systemImage: "checkmark.circle.fill") .font(.callout.weight(.medium)) .padding(.horizontal, 14) .padding(.vertical, 10) .background(.regularMaterial, in: Capsule()) .shadow(color: .black.opacity(0.13), radius: 16, y: 7) .accessibilityAddTraits(.isStaticText) } } struct MetricCard: View { let title: String let value: Int let systemImage: String var tint: Color = AppUI.accent var body: some View { HStack(spacing: 13) { Image(systemName: systemImage) .font(.title2) .foregroundStyle(tint) .frame(width: 36, height: 36) .background(tint.opacity(0.12), in: RoundedRectangle(cornerRadius: 9)) VStack(alignment: .leading, spacing: 2) { Text(value.formatted()) .font(.title2.weight(.semibold).monospacedDigit()) Text(title) .font(.caption) .foregroundStyle(.secondary) } Spacer(minLength: 0) } .padding(14) .background( Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: AppUI.cardCornerRadius) ) .overlay { RoundedRectangle(cornerRadius: AppUI.cardCornerRadius) .stroke(Color(nsColor: .separatorColor).opacity(0.55), lineWidth: 1) } } } struct RatingControl: View { let rating: Int? var size: Font = .body var onChange: ((Int?) -> Void)? var body: some View { HStack(spacing: 2) { ForEach(1...5, id: \.self) { value in Button { onChange?(rating == value ? nil : value) } label: { Image(systemName: value <= (rating ?? 0) ? "star.fill" : "star") .font(size) .foregroundStyle(value <= (rating ?? 0) ? AppUI.accent : .secondary) } .buttonStyle(.plain) .disabled(onChange == nil) .accessibilityLabel("\(value) von 5 Sternen") .accessibilityAddTraits(rating == value ? .isSelected : []) } } } } struct LoadingOverlay: View { let label: String var body: some View { VStack(spacing: 12) { ProgressView() .controlSize(.large) Text(label) .font(.callout) .foregroundStyle(.secondary) } .padding(24) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: AppUI.cardCornerRadius)) .shadow(color: .black.opacity(0.12), radius: 20, y: 8) } }