mana-swift-ui/Sources/ManaComponentsUI/ManaPill.swift
Till JS 9efa5291b4 feat(ManaPill): compact-Größe + .warning-Variante
`size: .regular | .compact` (12/7 Padding, .footnote.medium) für kleine
umbrechende Info-Tags/Chips, plus `.warning`-Variante (Warnfarbe getönt)
für hervorgehobene Hinweise. Additiv — bestehende Aufrufe unverändert
(Defaults .regular / Variante unverändert).

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

169 lines
5.5 KiB
Swift

import ManaTokens
import SwiftUI
/// Erscheinungsbild einer ``ManaPill``.
public enum ManaPillVariant: Sendable {
/// Aktiv/ausgewählt bzw. prominente Aktion — gefüllt in der Akzentfarbe.
case selected
/// Inaktiv bzw. neutrale Aktion — Surface-Fläche mit Rahmen.
case neutral
/// Destruktive Aktion (z. B. Löschen) — in der Fehlerfarbe getönt.
case destructive
/// Hervorgehobener Hinweis (z. B. zeitlich/aufmerksamkeitsstark) — in der
/// Warnfarbe getönt. Für Info-Tags, nicht für Aktionen.
case warning
}
/// Größe einer ``ManaPill``.
public enum ManaPillSize: Sendable {
/// Standard — großzügiges Touch-Target für Filter & Aktionen
/// (18/11 Padding, `.callout`/semibold). Referenz: seepuls-Filterleiste.
case regular
/// Kompakt — kleineres Info-Tag/Chip (12/7 Padding, `.footnote`/medium),
/// z. B. für umbrechende Attribut-/Ausstattungs-Reihen.
case compact
}
/// Einheitliche Kapsel-Pille (Filter-Chip oder Bulk-Aktion) für alle nativen
/// mana-Apps. **Größe und Form sind hier zentral festgelegt** — großzügiges
/// Touch-Target und `Capsule`-Form (Referenz: die seepuls-Filterleiste), damit
/// Filter-Chips und Auswahl-Aktionen über die ganze Flotte gleich groß sind.
///
/// Die Farben kommen aus der aktiven Theme-Variant über
/// `@Environment(\.manaTheme)`; die App injiziert sie an der Root via
/// `.manaTheme(_:)`. Es gibt bewusst **keine** Farb-Parameter — so bleibt jede
/// App bei ihrer Theme-Variant, ohne dass die Komponente Farben rät.
///
/// ```swift
/// // Bulk-Aktionen:
/// ManaPill("Zusammenfassen", systemImage: "sparkles", variant: .selected) { summarize() }
/// ManaPill("Archivieren", systemImage: "archivebox") { archive() }
/// ManaPill("Löschen", systemImage: "trash", variant: .destructive) { confirmDelete() }
///
/// // Toggle-Filter (isOn → .selected / .neutral):
/// ManaPill("Lesend", isOn: filter == .reading) { filter = .reading }
/// ```
public struct ManaPill: View {
@Environment(\.manaTheme) private var theme
private let title: String
private let systemImage: String?
private let variant: ManaPillVariant
private let size: ManaPillSize
private let action: (() -> Void)?
/// Erzeugt eine Pille mit explizitem `variant`.
///
/// - Parameters:
/// - title: Beschriftung.
/// - systemImage: optionales SF-Symbol vor der Beschriftung.
/// - variant: Erscheinungsbild (Default `.neutral`).
/// - size: Größe (Default `.regular`).
/// - action: optionaler Tap-Handler. `nil` → reines Label (nicht tappbar).
public init(
_ title: String,
systemImage: String? = nil,
variant: ManaPillVariant = .neutral,
size: ManaPillSize = .regular,
action: (() -> Void)? = nil
) {
self.title = title
self.systemImage = systemImage
self.variant = variant
self.size = size
self.action = action
}
/// Komfort-Init für Toggle-Filter: `isOn` mappt auf `.selected` bzw. `.neutral`.
///
/// - Parameters:
/// - title: Beschriftung.
/// - systemImage: optionales SF-Symbol vor der Beschriftung.
/// - isOn: ob der Filter aktiv ist.
/// - size: Größe (Default `.regular`).
/// - action: optionaler Tap-Handler.
public init(
_ title: String,
systemImage: String? = nil,
isOn: Bool,
size: ManaPillSize = .regular,
action: (() -> Void)? = nil
) {
self.init(
title,
systemImage: systemImage,
variant: isOn ? .selected : .neutral,
size: size,
action: action
)
}
public var body: some View {
if let action {
Button(action: action) { label }
.buttonStyle(.plain)
} else {
label
}
}
private var label: some View {
HStack(spacing: size == .compact ? Spacing.xxs : Spacing.xs) {
if let systemImage {
Image(systemName: systemImage)
}
Text(title)
}
.font(font)
.padding(.horizontal, size == .compact ? 12 : 18)
.padding(.vertical, size == .compact ? 7 : 11)
.foregroundStyle(foregroundColor)
.background(backgroundColor)
.clipShape(Capsule())
.overlay {
if showsBorder {
Capsule().strokeBorder(borderColor, lineWidth: 1)
}
}
.contentShape(Capsule())
}
private var colors: ManaThemeColors { theme.colors }
private var font: Font {
switch size {
case .regular: .callout.weight(.semibold)
case .compact: .footnote.weight(.medium)
}
}
private var backgroundColor: Color {
switch variant {
case .selected: colors.primary
case .neutral: colors.surface
case .destructive: colors.error.opacity(0.12)
case .warning: colors.warning.opacity(0.14)
}
}
private var foregroundColor: Color {
switch variant {
case .selected: colors.primaryForeground
case .neutral: colors.foreground
case .destructive: colors.error
case .warning: colors.warning
}
}
private var showsBorder: Bool {
variant != .selected
}
private var borderColor: Color {
switch variant {
case .destructive: colors.error.opacity(0.4)
case .warning: colors.warning.opacity(0.35)
default: colors.border
}
}
}