97 lines
4.1 KiB
Swift
97 lines
4.1 KiB
Swift
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() }
|
|
}
|