2026-04-19 16:29:13 +02:00
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
|
struct PrimaryActionStyle: ButtonStyle {
|
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
|
|
|
PrimaryActionBody(configuration: configuration)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct PrimaryActionBody: View {
|
|
|
|
|
let configuration: Configuration
|
|
|
|
|
@Environment(\.isEnabled) private var isEnabled
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
configuration.label
|
2026-04-19 21:50:03 +02:00
|
|
|
.font(.subheadline.weight(.semibold))
|
2026-04-19 16:29:13 +02:00
|
|
|
.frame(maxWidth: .infinity)
|
|
|
|
|
.padding(.horizontal, 18)
|
2026-04-19 21:50:03 +02:00
|
|
|
.frame(height: 44)
|
|
|
|
|
.foregroundStyle(Color.idpPrimaryForeground)
|
2026-04-19 16:29:13 +02:00
|
|
|
.background(
|
|
|
|
|
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
2026-04-19 21:50:03 +02:00
|
|
|
.fill(isEnabled ? Color.idpPrimary : Color.idpMuted)
|
2026-04-19 16:29:13 +02:00
|
|
|
)
|
2026-04-19 21:50:03 +02:00
|
|
|
.opacity(configuration.isPressed ? 0.9 : 1)
|
|
|
|
|
.scaleEffect(configuration.isPressed ? 0.99 : 1)
|
|
|
|
|
.animation(.easeOut(duration: 0.12), value: configuration.isPressed)
|
2026-04-19 16:29:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct SecondaryActionStyle: ButtonStyle {
|
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
|
|
|
configuration.label
|
2026-04-19 21:50:03 +02:00
|
|
|
.font(.subheadline.weight(.semibold))
|
2026-04-19 16:29:13 +02:00
|
|
|
.frame(maxWidth: .infinity)
|
|
|
|
|
.padding(.horizontal, 18)
|
2026-04-19 21:50:03 +02:00
|
|
|
.frame(height: 44)
|
2026-04-19 16:29:13 +02:00
|
|
|
.foregroundStyle(.primary)
|
|
|
|
|
.background(
|
|
|
|
|
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
2026-04-19 21:50:03 +02:00
|
|
|
.fill(Color.clear)
|
2026-04-19 16:29:13 +02:00
|
|
|
)
|
|
|
|
|
.overlay(
|
|
|
|
|
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
2026-04-19 21:50:03 +02:00
|
|
|
.stroke(Color.idpBorder, lineWidth: 1)
|
2026-04-19 16:29:13 +02:00
|
|
|
)
|
2026-04-19 21:50:03 +02:00
|
|
|
.opacity(configuration.isPressed ? 0.85 : 1)
|
|
|
|
|
.scaleEffect(configuration.isPressed ? 0.99 : 1)
|
|
|
|
|
.animation(.easeOut(duration: 0.12), value: configuration.isPressed)
|
2026-04-19 16:29:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct DestructiveStyle: ButtonStyle {
|
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
|
|
|
configuration.label
|
2026-04-19 21:50:03 +02:00
|
|
|
.font(.subheadline.weight(.semibold))
|
2026-04-19 16:29:13 +02:00
|
|
|
.frame(maxWidth: .infinity)
|
|
|
|
|
.padding(.horizontal, 18)
|
2026-04-19 21:50:03 +02:00
|
|
|
.frame(height: 44)
|
|
|
|
|
.foregroundStyle(.white)
|
2026-04-19 16:29:13 +02:00
|
|
|
.background(
|
|
|
|
|
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
2026-04-19 21:50:03 +02:00
|
|
|
.fill(Color.idpDestructive)
|
2026-04-19 16:29:13 +02:00
|
|
|
)
|
2026-04-19 21:50:03 +02:00
|
|
|
.opacity(configuration.isPressed ? 0.9 : 1)
|
|
|
|
|
.scaleEffect(configuration.isPressed ? 0.99 : 1)
|
|
|
|
|
.animation(.easeOut(duration: 0.12), value: configuration.isPressed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct AccentActionStyle: ButtonStyle {
|
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
|
|
|
configuration.label
|
|
|
|
|
.font(.subheadline.weight(.semibold))
|
|
|
|
|
.frame(maxWidth: .infinity)
|
|
|
|
|
.padding(.horizontal, 18)
|
|
|
|
|
.frame(height: 44)
|
|
|
|
|
.foregroundStyle(.white)
|
|
|
|
|
.background(
|
2026-04-19 16:29:13 +02:00
|
|
|
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
2026-04-19 21:50:03 +02:00
|
|
|
.fill(IdP.tint)
|
2026-04-19 16:29:13 +02:00
|
|
|
)
|
|
|
|
|
.opacity(configuration.isPressed ? 0.9 : 1)
|
2026-04-19 21:50:03 +02:00
|
|
|
.scaleEffect(configuration.isPressed ? 0.99 : 1)
|
|
|
|
|
.animation(.easeOut(duration: 0.12), value: configuration.isPressed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct GhostActionStyle: ButtonStyle {
|
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
|
|
|
configuration.label
|
|
|
|
|
.font(.subheadline.weight(.medium))
|
|
|
|
|
.padding(.horizontal, 12)
|
|
|
|
|
.frame(height: 32)
|
|
|
|
|
.foregroundStyle(.primary)
|
|
|
|
|
.background(
|
|
|
|
|
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
|
|
|
|
.fill(configuration.isPressed ? Color.idpMuted : Color.clear)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ShadcnBadge: View {
|
|
|
|
|
enum Tone {
|
|
|
|
|
case neutral
|
|
|
|
|
case outline
|
|
|
|
|
case ok
|
|
|
|
|
case warn
|
|
|
|
|
case danger
|
|
|
|
|
case accent
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let title: String
|
|
|
|
|
var tone: Tone = .neutral
|
|
|
|
|
var leading: Image? = nil
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
HStack(spacing: 4) {
|
|
|
|
|
if let leading {
|
|
|
|
|
leading
|
|
|
|
|
.font(.caption2.weight(.semibold))
|
|
|
|
|
}
|
|
|
|
|
Text(title)
|
|
|
|
|
}
|
|
|
|
|
.font(.caption2.weight(.semibold))
|
|
|
|
|
.padding(.horizontal, 8)
|
|
|
|
|
.padding(.vertical, 3)
|
|
|
|
|
.foregroundStyle(foreground)
|
|
|
|
|
.background(background, in: Capsule(style: .continuous))
|
|
|
|
|
.overlay(
|
|
|
|
|
Capsule(style: .continuous)
|
|
|
|
|
.stroke(strokeColor, lineWidth: strokeWidth)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var foreground: Color {
|
|
|
|
|
switch tone {
|
|
|
|
|
case .neutral: return Color.idpMutedForeground
|
|
|
|
|
case .outline: return .primary
|
|
|
|
|
case .ok: return Color.idpOK
|
|
|
|
|
case .warn: return Color(red: 0.52, green: 0.30, blue: 0.05)
|
|
|
|
|
case .danger: return Color(red: 0.60, green: 0.10, blue: 0.10)
|
|
|
|
|
case .accent: return IdP.tint
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var background: Color {
|
|
|
|
|
switch tone {
|
|
|
|
|
case .neutral: return Color.idpMuted
|
|
|
|
|
case .outline: return .clear
|
|
|
|
|
case .ok: return Color.idpOK.opacity(0.12)
|
|
|
|
|
case .warn: return Color.idpWarn.opacity(0.18)
|
|
|
|
|
case .danger: return Color.idpDestructive.opacity(0.14)
|
|
|
|
|
case .accent: return Color.idpAccentSoft
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var strokeColor: Color {
|
|
|
|
|
tone == .outline ? Color.idpBorder : .clear
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var strokeWidth: CGFloat {
|
|
|
|
|
tone == .outline ? 1 : 0
|
2026-04-19 16:29:13 +02:00
|
|
|
}
|
|
|
|
|
}
|