mana-swift-ui/Sources/ManaThemeUI/ManaCard.swift
Till JS c16694fc8e fix(ManaAccountView): full-width Karten ohne abgeschnittenen Border
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>
2026-06-05 23:37:50 +02:00

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