- 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>
102 lines
4.3 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|
|
}
|