Statt die randlosen Karten einzuruecken (zu schmal, Feedback Till): Anmelde- CTA + Feedback-Karte verzichten full-bleed jetzt auf den Manual-Border und definieren sich ueber Surface-Kontrast wie eine native grouped Section -> volle Section-Breite, keine an den Ecken abgeschnittenen Ecken mehr. Empirisch verifiziert: die inset-grouped Form-Maske clippt den Zelleninhalt unabhaengig vom Karten-Eckradius (4/10/12pt = identische Ecken), ein Overlay- strokeBorder laesst sich full-bleed nicht sauber schliessen. manaCardListRow() kapselt jetzt nur noch die full-bleed-Row-Konfiguration + dokumentiert die Regel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
5.5 KiB
Swift
124 lines
5.5 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
|
|
))
|
|
}
|
|
}
|
|
|
|
public extension View {
|
|
/// Platziert eine ``SwiftUICore/View/manaCard(surface:border:cornerRadius:borderWidth:padding:alignment:)``-Karte
|
|
/// **full-bleed** als Zeile in einer `Form`/`List` — volle Section-Breite
|
|
/// (wie eine native grouped Section), transparenter Row-Background, keine
|
|
/// Eigen-Insets.
|
|
///
|
|
/// **Karte ohne Manual-Border verwenden.** Eine inset-grouped `Form`/`List`
|
|
/// maskiert ihre Section auf gerundete Ecken und clippt den Zelleninhalt
|
|
/// auf diese Maske — **unabhängig vom Karten-Eckradius** (empirisch: 4 / 10
|
|
/// / 12 pt liefern identische Ecken, die Maske dominiert). Ein Overlay-
|
|
/// `strokeBorder` wird dadurch an den vier Ecken abgeschnitten und lässt
|
|
/// sich full-bleed **nicht** sauber schließen. Lösung: die Karte über
|
|
/// **Surface-Kontrast** definieren (`.manaCard(surface:)` ohne `border:`),
|
|
/// genau wie eine native grouped Section. Wer einen sichtbaren Rundum-Border
|
|
/// braucht, darf die Karte **nicht** full-bleed setzen (dann eingerückt mit
|
|
/// eigener `.manaCard(border:)`), büßt aber die volle Breite ein.
|
|
func manaCardListRow() -> some View {
|
|
listRowInsets(EdgeInsets())
|
|
.listRowBackground(Color.clear)
|
|
}
|
|
}
|
|
|
|
/// `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)
|
|
}
|
|
}
|
|
}
|
|
}
|