mana-swift-ui/Sources/ManaThemeUI/ManaCard.swift
Till JS 93355c1e10 Karten-Chrome vereinheitlichen: .manaCard + RoundedRectangle.mana + ManaAboutSection
- ManaThemeUI: geteiltes Karten-Chrome `.manaCard(...)` (continuous Squircle +
  optionaler Border aus EINER gemeinsamen Form, saubere Ecken) und
  `RoundedRectangle.mana(Radius.*)` als geteilte Ecken-Sprache.
- ManaSignInCTA (Login) + ManaFeedbackCard ziehen auf `.manaCard` — identisches
  Chrome, Login bekommt denselben Border, beide continuous.
- Rest-Chrome (Eingabefelder, Primary-Button, Feedback-Buttons, 2FA-QR/Codes/
  Kopier-Buttons) auf `RoundedRectangle.mana(Radius.*)`; keine cornerRadius-
  Literale mehr, Radien auf Token-Skala konvergiert.
- ManaAboutSection: vereinheitlichte „Über"-Sektion (App/Version/Build + Links),
  opt-in via `ManaAccountView(about:)`.

Kein API-Bruch (alles additiv/optisch). CHANGELOG aktualisiert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:26:26 +02:00

102 lines
4.3 KiB
Swift

import ManaTokens
import SwiftUI
/// Vereinheitlichtes **Karten-Chrome** für alle Verein-Apps: gefüllte
/// `surface`-Fläche, durchgehende **continuous** (Apple-Squircle-)Ecken und
/// ein optionaler Hairline-Rahmen in `border`-Farbe. Eine einzige Quelle für
/// die Optik, damit Login-CTA, Feedback-Karte, Info-Blöcke usw. nicht
/// auseinanderdriften (vorher: jede Karte hardcoded eigenen `cornerRadius`,
/// mal mit, mal ohne Border).
///
/// **Warum `.continuous`?** SwiftUIs Default-`RoundedRectangle` zeichnet
/// `.circular` — ein simpler Kreisbogen pro Ecke. Apples System-Optik (App-
/// Icons, Grouped-Cells, Sheets) nutzt dagegen den *Squircle*: einen
/// Superellipsen-Übergang, bei dem die Krümmung weich in die Kante ausläuft
/// statt abrupt am Bogenende. `RoundedRectangle(cornerRadius:style:.continuous)`
/// liefert genau das (seit iOS 13, voll verfügbar ab unserem iOS-18-Minimum).
/// Optisch ruhiger, „teurer", und konsistent mit dem Rest des Systems.
///
/// **Warum eine geteilte Form-Instanz für Füllung UND Rahmen?** Wenn
/// Hintergrund (`.background(_:in:)`) und Border (`.overlay(...strokeBorder)`)
/// aus zwei *getrennt* erzeugten Shapes kommen — erst recht mit
/// unterschiedlichem Corner-Style —, sitzt der Rahmen subtil „neben" der
/// Füllung und folgt den Ecken nicht sauber. Hier teilen sich beide *dieselbe*
/// `.continuous`-Shape, und `strokeBorder` zeichnet den Strich **innerhalb** der
/// Bounds (nicht mittig auf der Kante wie `stroke`) — so deckt der Rahmen die
/// Ecken exakt ab.
///
/// Theming-agnostisch: die Farben werden explizit übergeben, damit der
/// Modifier sowohl für `ManaBrandConfig`-getönte (`ManaAuthUI`) als auch für
/// `\.manaTheme`-getönte (`ManaFeedbackUI`) Views funktioniert. Radius-Defaults
/// kommen aus ``ManaTokens/Radius``.
public extension View {
/// Legt das Vereins-Karten-Chrome um diese View.
///
/// - Parameters:
/// - surface: Füllfarbe (typisch `theme.surface` / `brand.surface`).
/// - border: Rahmenfarbe; `nil` = rahmenlos. Default `nil`.
/// - cornerRadius: Eckenradius. Default ``ManaTokens/Radius/lg`` (12pt).
/// - borderWidth: Strichstärke des Rahmens. Default 1pt.
/// - padding: Innenabstand des Karteninhalts. Default 16pt.
/// - alignment: Ausrichtung des Inhalts in der vollbreiten Karte.
func manaCard(
surface: Color,
border: Color? = nil,
cornerRadius: CGFloat = Radius.lg,
borderWidth: CGFloat = 1,
padding: CGFloat = 16,
alignment: Alignment = .leading
) -> some View {
modifier(ManaCardModifier(
surface: surface,
border: border,
cornerRadius: cornerRadius,
borderWidth: borderWidth,
padding: padding,
alignment: alignment
))
}
}
/// `ViewModifier` hinter ``SwiftUICore/View/manaCard(surface:border:cornerRadius:borderWidth:padding:alignment:)``.
/// Direkt nur nutzen, wenn du den Modifier programmatisch komponieren musst;
/// im Normalfall die `.manaCard(…)`-Methode verwenden.
public struct ManaCardModifier: ViewModifier {
private let surface: Color
private let border: Color?
private let cornerRadius: CGFloat
private let borderWidth: CGFloat
private let padding: CGFloat
private let alignment: Alignment
public init(
surface: Color,
border: Color? = nil,
cornerRadius: CGFloat = Radius.lg,
borderWidth: CGFloat = 1,
padding: CGFloat = 16,
alignment: Alignment = .leading
) {
self.surface = surface
self.border = border
self.cornerRadius = cornerRadius
self.borderWidth = borderWidth
self.padding = padding
self.alignment = alignment
}
public func body(content: Content) -> some View {
// Eine durchgehende `.continuous`-Form für Füllung UND Rahmen — gleiche
// Instanz, damit der Border den Squircle-Ecken sauber folgt.
let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
content
.padding(padding)
.frame(maxWidth: .infinity, alignment: alignment)
.background(surface, in: shape)
.overlay {
if let border {
shape.strokeBorder(border, lineWidth: borderWidth)
}
}
}
}