Bring the SwiftUI app in line with the Apple-native mock and keep pending approvals actionable from Live Activities and watch complications.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
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
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.vertical, 14)
|
||||
.foregroundStyle(.white)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
||||
.fill(isEnabled ? IdP.tint : Color.secondary.opacity(0.25))
|
||||
)
|
||||
.opacity(configuration.isPressed ? 0.92 : 1)
|
||||
.scaleEffect(configuration.isPressed ? 0.985 : 1)
|
||||
.animation(.easeOut(duration: 0.16), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SecondaryActionStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.vertical, 14)
|
||||
.foregroundStyle(.primary)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
||||
.fill(Color.idpSecondaryGroupedBackground)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
||||
.stroke(Color.idpSeparator.opacity(0.55), lineWidth: 1)
|
||||
)
|
||||
.opacity(configuration.isPressed ? 0.9 : 1)
|
||||
.scaleEffect(configuration.isPressed ? 0.985 : 1)
|
||||
.animation(.easeOut(duration: 0.16), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
struct DestructiveStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.vertical, 14)
|
||||
.foregroundStyle(.red)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
||||
.fill(Color.red.opacity(0.10))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
||||
.stroke(Color.red.opacity(0.18), lineWidth: 1)
|
||||
)
|
||||
.opacity(configuration.isPressed ? 0.9 : 1)
|
||||
.scaleEffect(configuration.isPressed ? 0.985 : 1)
|
||||
.animation(.easeOut(duration: 0.16), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ApprovalCardModifier: ViewModifier {
|
||||
var highlighted = false
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.padding(18)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous)
|
||||
.fill(Color.idpSecondaryGroupedBackground)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous)
|
||||
.stroke(highlighted ? IdP.tint.opacity(0.7) : Color.idpSeparator.opacity(0.55), lineWidth: highlighted ? 1.5 : 1)
|
||||
)
|
||||
.overlay {
|
||||
if highlighted {
|
||||
RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous)
|
||||
.stroke(IdP.tint.opacity(0.12), lineWidth: 6)
|
||||
.padding(-2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func approvalCard(highlighted: Bool = false) -> some View {
|
||||
modifier(ApprovalCardModifier(highlighted: highlighted))
|
||||
}
|
||||
|
||||
func deviceRowStyle() -> some View {
|
||||
modifier(DeviceRowStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct RequestHeroCard: View {
|
||||
let request: ApprovalRequest
|
||||
let handle: String
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 16) {
|
||||
MonogramAvatar(title: request.source, size: 64)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("\(request.source) wants to sign in as you")
|
||||
.font(.title3.weight(.semibold))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text("Continue as \(Text(handle).foregroundStyle(IdP.tint))")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Label(request.kind.title, systemImage: request.kind.systemImage)
|
||||
Text(request.createdAt, style: .relative)
|
||||
}
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.approvalCard(highlighted: true)
|
||||
}
|
||||
}
|
||||
|
||||
struct MonogramAvatar: View {
|
||||
let title: String
|
||||
var size: CGFloat = 40
|
||||
var tint: Color = IdP.tint
|
||||
|
||||
private var monogram: String {
|
||||
String(title.trimmingCharacters(in: .whitespacesAndNewlines).first ?? "I").uppercased()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: size * 0.34, style: .continuous)
|
||||
.fill(tint.opacity(0.14))
|
||||
|
||||
Image("AppMonogram")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: size * 0.44, height: size * 0.44)
|
||||
.opacity(0.18)
|
||||
|
||||
Text(monogram)
|
||||
.font(.system(size: size * 0.42, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(tint)
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
struct DeviceRowStyle: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import SwiftUI
|
||||
|
||||
public extension View {
|
||||
@ViewBuilder
|
||||
func idpGlassChrome() -> some View {
|
||||
if #available(iOS 26, macOS 26, *) {
|
||||
self.glassEffect(.regular)
|
||||
} else {
|
||||
self.background(.regularMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IdPGlassCapsule<Content: View>: View {
|
||||
let padding: EdgeInsets
|
||||
let content: Content
|
||||
|
||||
init(
|
||||
padding: EdgeInsets = EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16),
|
||||
@ViewBuilder content: () -> Content
|
||||
) {
|
||||
self.padding = padding
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
content
|
||||
.padding(padding)
|
||||
.background(
|
||||
Capsule(style: .continuous)
|
||||
.fill(.clear)
|
||||
.idpGlassChrome()
|
||||
)
|
||||
.overlay(
|
||||
Capsule(style: .continuous)
|
||||
.stroke(Color.white.opacity(0.16), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import SwiftUI
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
enum Haptics {
|
||||
static func success() {
|
||||
#if os(iOS)
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
#elseif os(macOS)
|
||||
NSHapticFeedbackManager.defaultPerformer.perform(.levelChange, performanceTime: .now)
|
||||
#endif
|
||||
}
|
||||
|
||||
static func warning() {
|
||||
#if os(iOS)
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.warning)
|
||||
#elseif os(macOS)
|
||||
NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .now)
|
||||
#endif
|
||||
}
|
||||
|
||||
static func selection() {
|
||||
#if os(iOS)
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
#elseif os(macOS)
|
||||
NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .now)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import SwiftUI
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#elseif canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
public enum IdP {
|
||||
public static let tint = Color("IdPTint")
|
||||
public static let cardRadius: CGFloat = 22
|
||||
public static let controlRadius: CGFloat = 14
|
||||
public static let badgeRadius: CGFloat = 8
|
||||
|
||||
static func horizontalPadding(compact: Bool) -> CGFloat {
|
||||
compact ? 16 : 24
|
||||
}
|
||||
|
||||
static func verticalPadding(compact: Bool) -> CGFloat {
|
||||
compact ? 16 : 24
|
||||
}
|
||||
}
|
||||
|
||||
extension Color {
|
||||
static var idpGroupedBackground: Color {
|
||||
#if os(macOS)
|
||||
Color(nsColor: .windowBackgroundColor)
|
||||
#elseif os(watchOS)
|
||||
.black
|
||||
#else
|
||||
Color(uiColor: .systemGroupedBackground)
|
||||
#endif
|
||||
}
|
||||
|
||||
static var idpSecondaryGroupedBackground: Color {
|
||||
#if os(macOS)
|
||||
Color(nsColor: .controlBackgroundColor)
|
||||
#elseif os(watchOS)
|
||||
Color.white.opacity(0.08)
|
||||
#else
|
||||
Color(uiColor: .secondarySystemGroupedBackground)
|
||||
#endif
|
||||
}
|
||||
|
||||
static var idpTertiaryFill: Color {
|
||||
#if os(macOS)
|
||||
Color(nsColor: .quaternaryLabelColor).opacity(0.08)
|
||||
#elseif os(watchOS)
|
||||
Color.white.opacity(0.12)
|
||||
#else
|
||||
Color(uiColor: .tertiarySystemFill)
|
||||
#endif
|
||||
}
|
||||
|
||||
static var idpSeparator: Color {
|
||||
#if os(macOS)
|
||||
Color(nsColor: .separatorColor)
|
||||
#elseif os(watchOS)
|
||||
Color.white.opacity(0.14)
|
||||
#else
|
||||
Color(uiColor: .separator)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func idpScreenPadding(compact: Bool) -> some View {
|
||||
padding(.horizontal, IdP.horizontalPadding(compact: compact))
|
||||
.padding(.vertical, IdP.verticalPadding(compact: compact))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func idpInlineNavigationTitle() -> some View {
|
||||
#if os(macOS)
|
||||
self
|
||||
#else
|
||||
navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func idpTabBarChrome() -> some View {
|
||||
#if os(macOS)
|
||||
self
|
||||
#else
|
||||
toolbarBackground(.visible, for: .tabBar)
|
||||
.toolbarBackground(.regularMaterial, for: .tabBar)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func idpSearchable(text: Binding<String>, isPresented: Binding<Bool>) -> some View {
|
||||
#if os(macOS)
|
||||
searchable(text: text, isPresented: isPresented)
|
||||
#else
|
||||
searchable(text: text, isPresented: isPresented, placement: .navigationBarDrawer(displayMode: .always))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension ToolbarItemPlacement {
|
||||
static var idpTrailingToolbar: ToolbarItemPlacement {
|
||||
#if os(macOS)
|
||||
.primaryAction
|
||||
#else
|
||||
.topBarTrailing
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import SwiftUI
|
||||
|
||||
struct StatusDot: View {
|
||||
let color: Color
|
||||
|
||||
var body: some View {
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: 10, height: 10)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.65), lineWidth: 1)
|
||||
)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user