Files
scan-receipts/app/ios/ScanReceipts/Design/ZenithButtonStyle.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() }
}