import SwiftUI /// Buttons per `DESIGN (1).md` → Components → Buttons: "Solid #000000 /// background with #FFFFFF text for primary actions. 1px solid #000000 /// border with no fill for secondary. All buttons are rectangular [0px /// radius] with no padding-inline under 24px." And → Elevation: "On hover, /// elements do not lift; they shift color... a button fill turns from Black /// to Slate Blue-Grey" — the pressed state here plays the role "hover" plays /// on the web. /// Every Zenith button style dims to this opacity when `.disabled(true)` — /// centralised here so a disabled `.zenithPrimary` etc. dims automatically, /// the way `.borderedProminent`/`.bordered` do out of the box. Without this, /// each call site would need its own manual `.opacity(isEnabled ? 1 : 0.4)`, /// which is easy to forget (an earlier pass of this app's screens did, in /// fact, forget it in a couple of places before this was added centrally). private let zenithDisabledOpacity: Double = 0.4 struct ZenithPrimaryButtonStyle: ButtonStyle { var isDestructive = false @Environment(\.isEnabled) private var isEnabled func makeBody(configuration: Configuration) -> some View { configuration.label .font(.zenithBodyMd) .fontWeight(.medium) .foregroundStyle(.white) .frame(maxWidth: .infinity, minHeight: 50) .padding(.horizontal, ZenithSpacing.md) .background(fill(pressed: configuration.isPressed)) // Sharp corners are the point of this system — a plain // Rectangle fill, never `.cornerRadius`. .opacity(isEnabled ? 1 : zenithDisabledOpacity) } private func fill(pressed: Bool) -> Color { if isDestructive { return pressed ? Color.zenithError.opacity(0.8) : .zenithError } return pressed ? .zenithSlate : .zenithBlack } } struct ZenithSecondaryButtonStyle: ButtonStyle { var isDestructive = false @Environment(\.isEnabled) private var isEnabled func makeBody(configuration: Configuration) -> some View { configuration.label .font(.zenithBodyMd) .fontWeight(.medium) .foregroundStyle(isDestructive ? Color.zenithError : Color.zenithBlack) .frame(maxWidth: .infinity, minHeight: 50) .padding(.horizontal, ZenithSpacing.md) .background(Color.zenithSurface) .overlay( // Border THICKENS on press instead of the fill lifting — // same "no lift, shift instead" rule as the primary style. Rectangle() .strokeBorder( isDestructive ? Color.zenithError : Color.zenithBlack, lineWidth: configuration.isPressed ? 2 : 1 ) ) .opacity(isEnabled ? 1 : zenithDisabledOpacity) } } /// A plain-text button with no fill/border at all — for tertiary actions /// ("Noch kein Konto? Registrieren", "Abbrechen") that shouldn't compete /// visually with a screen's primary/secondary buttons. struct ZenithPlainButtonStyle: ButtonStyle { var color: Color = .zenithMuted @Environment(\.isEnabled) private var isEnabled func makeBody(configuration: Configuration) -> some View { configuration.label .font(.zenithBodySm) .foregroundStyle(configuration.isPressed ? color.opacity(0.6) : color) .opacity(isEnabled ? 1 : zenithDisabledOpacity) } } extension ButtonStyle where Self == ZenithPrimaryButtonStyle { static var zenithPrimary: ZenithPrimaryButtonStyle { ZenithPrimaryButtonStyle() } static var zenithPrimaryDestructive: ZenithPrimaryButtonStyle { ZenithPrimaryButtonStyle(isDestructive: true) } } extension ButtonStyle where Self == ZenithSecondaryButtonStyle { static var zenithSecondary: ZenithSecondaryButtonStyle { ZenithSecondaryButtonStyle() } static var zenithSecondaryDestructive: ZenithSecondaryButtonStyle { ZenithSecondaryButtonStyle(isDestructive: true) } } extension ButtonStyle where Self == ZenithPlainButtonStyle { static var zenithPlain: ZenithPlainButtonStyle { ZenithPlainButtonStyle() } }