`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>
169 lines
5.5 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|