Some checks are pending
CI / test (push) Waiting to run
Tighten the inbox, detail, and watch layouts so approval actions feel denser and more direct across compact surfaces.
133 lines
4.4 KiB
Swift
133 lines
4.4 KiB
Swift
import SwiftUI
|
|
|
|
struct ApprovalCardModifier: ViewModifier {
|
|
var highlighted = false
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.padding(14)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous)
|
|
.fill(Color.idpCard)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous)
|
|
.stroke(highlighted ? IdP.tint : Color.idpBorder, lineWidth: 1)
|
|
)
|
|
.background(
|
|
highlighted
|
|
? RoundedRectangle(cornerRadius: IdP.cardRadius + 3, style: .continuous)
|
|
.fill(IdP.tint.opacity(0.10))
|
|
.padding(-3)
|
|
: nil
|
|
)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
VStack(spacing: 12) {
|
|
MonogramAvatar(
|
|
title: request.source,
|
|
size: 52,
|
|
tint: BrandTint.color(for: request.source),
|
|
filled: true
|
|
)
|
|
|
|
VStack(spacing: 4) {
|
|
Text("\(request.source) wants to sign in")
|
|
.font(.title3.weight(.bold))
|
|
.multilineTextAlignment(.center)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
HStack(spacing: 4) {
|
|
Text("requesting")
|
|
Text(handle)
|
|
.foregroundStyle(IdP.tint)
|
|
.fontWeight(.semibold)
|
|
Text("·")
|
|
Text(request.createdAt, style: .relative)
|
|
}
|
|
.font(.footnote)
|
|
.foregroundStyle(Color.idpMutedForeground)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 6)
|
|
.approvalCard(highlighted: false)
|
|
}
|
|
}
|
|
|
|
struct MonogramAvatar: View {
|
|
let title: String
|
|
var size: CGFloat = 40
|
|
var tint: Color = IdP.tint
|
|
/// Shadcn-style filled tile (brand color bg, white text).
|
|
/// When false, renders the legacy tinted-pastel avatar.
|
|
var filled: Bool = true
|
|
|
|
private var monogram: String {
|
|
let letters = title
|
|
.replacingOccurrences(of: "auth.", with: "")
|
|
.split { !$0.isLetter && !$0.isNumber }
|
|
.prefix(2)
|
|
.compactMap { $0.first }
|
|
let glyph = String(letters.map(Character.init))
|
|
return glyph.isEmpty ? "I" : glyph.uppercased()
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
.fill(filled ? tint : tint.opacity(0.14))
|
|
|
|
Text(monogram)
|
|
.font(.system(size: size * (monogram.count > 1 ? 0.36 : 0.44),
|
|
weight: .bold,
|
|
design: .default))
|
|
.foregroundStyle(filled ? .white : tint)
|
|
.tracking(-0.3)
|
|
}
|
|
.frame(width: size, height: size)
|
|
.accessibilityHidden(true)
|
|
}
|
|
}
|
|
|
|
enum BrandTint {
|
|
static func color(for host: String) -> Color {
|
|
let key = host.lowercased()
|
|
if key.contains("github") { return Color(red: 0.14, green: 0.16, blue: 0.18) }
|
|
if key.contains("lufthansa") { return Color(red: 0.02, green: 0.09, blue: 0.30) }
|
|
if key.contains("hetzner") { return Color(red: 0.84, green: 0.05, blue: 0.18) }
|
|
if key.contains("notion") { return Color(red: 0.12, green: 0.12, blue: 0.12) }
|
|
if key.contains("apple") { return Color(red: 0.18, green: 0.18, blue: 0.22) }
|
|
if key.contains("reddit") { return Color(red: 1.00, green: 0.27, blue: 0.00) }
|
|
if key.contains("cli") { return Color(red: 0.24, green: 0.26, blue: 0.32) }
|
|
if key.contains("workspace") { return Color(red: 0.26, green: 0.38, blue: 0.88) }
|
|
if key.contains("foss") || key.contains("berlin-mbp") {
|
|
return Color(red: 0.12, green: 0.45, blue: 0.70)
|
|
}
|
|
return IdP.tint
|
|
}
|
|
}
|
|
|
|
struct DeviceRowStyle: ViewModifier {
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.padding(.vertical, 2)
|
|
}
|
|
}
|