import SwiftUI struct RequestList: View { let requests: [ApprovalRequest] let compactLayout: Bool let activeRequestID: ApprovalRequest.ID? let onApprove: ((ApprovalRequest) -> Void)? let onReject: ((ApprovalRequest) -> Void)? let onOpenRequest: (ApprovalRequest) -> Void var body: some View { VStack(spacing: 14) { ForEach(requests) { request in RequestCard( request: request, compactLayout: compactLayout, isBusy: activeRequestID == request.id, onApprove: onApprove == nil ? nil : { onApprove?(request) }, onReject: onReject == nil ? nil : { onReject?(request) }, onOpenRequest: { onOpenRequest(request) } ) } } } } private struct RequestCard: View { let request: ApprovalRequest let compactLayout: Bool let isBusy: Bool let onApprove: (() -> Void)? let onReject: (() -> Void)? let onOpenRequest: () -> Void var body: some View { VStack(alignment: .leading, spacing: 12) { HStack(alignment: .top, spacing: 12) { Image(systemName: request.kind.systemImage) .font(.headline) .foregroundStyle(requestAccent) .frame(width: 28, height: 28) VStack(alignment: .leading, spacing: 4) { Text(request.title) .font(.headline) .multilineTextAlignment(.leading) Text(request.source) .font(.subheadline) .foregroundStyle(.secondary) .lineLimit(1) } Spacer(minLength: 0) AppStatusTag(title: request.status.title, tone: statusTone) } Text(request.subtitle) .font(.subheadline) .foregroundStyle(.secondary) .lineLimit(2) HStack(spacing: 8) { AppStatusTag(title: request.risk.title, tone: request.risk == .routine ? dashboardAccent : .orange) Text(request.scopeSummary) .font(.footnote) .foregroundStyle(.secondary) Spacer(minLength: 0) Text(request.createdAt, style: .relative) .font(.footnote) .foregroundStyle(.secondary) } if !request.scopes.isEmpty { Text("Proof details: \(request.scopes.joined(separator: ", "))") .font(.footnote) .foregroundStyle(.secondary) .lineLimit(2) } controls } .padding(compactLayout ? 18 : 20) .appSurface(radius: 24) } @ViewBuilder private var controls: some View { if compactLayout { VStack(alignment: .leading, spacing: 10) { reviewButton decisionButtons } } else { HStack(spacing: 12) { reviewButton Spacer(minLength: 0) decisionButtons } } } private var reviewButton: some View { Button { onOpenRequest() } label: { Label("Review proof", systemImage: "arrow.up.forward.app") } .buttonStyle(.bordered) } @ViewBuilder private var decisionButtons: some View { if request.status == .pending, let onApprove, let onReject { Button { onApprove() } label: { if isBusy { ProgressView() } else { Label("Verify", systemImage: "checkmark.circle.fill") } } .buttonStyle(.borderedProminent) .disabled(isBusy) Button(role: .destructive) { onReject() } label: { Label("Decline", systemImage: "xmark.circle.fill") } .buttonStyle(.bordered) .disabled(isBusy) } } private var statusTone: Color { switch request.status { case .pending: .orange case .approved: .green case .rejected: .red } } private var requestAccent: Color { switch request.status { case .approved: .green case .rejected: .red case .pending: request.risk == .routine ? dashboardAccent : .orange } } } struct NotificationList: View { let notifications: [AppNotification] let compactLayout: Bool let onMarkRead: (AppNotification) -> Void var body: some View { VStack(spacing: 14) { ForEach(notifications) { notification in NotificationCard( notification: notification, compactLayout: compactLayout, onMarkRead: { onMarkRead(notification) } ) } } } } private struct NotificationCard: View { let notification: AppNotification let compactLayout: Bool let onMarkRead: () -> Void var body: some View { VStack(alignment: .leading, spacing: 10) { HStack(alignment: .top, spacing: 12) { Image(systemName: notification.kind.systemImage) .font(.headline) .foregroundStyle(accentColor) .frame(width: 28, height: 28) VStack(alignment: .leading, spacing: 4) { Text(notification.title) .font(.headline) HStack(spacing: 8) { AppStatusTag(title: notification.kind.title, tone: accentColor) if notification.isUnread { AppStatusTag(title: "Unread", tone: .orange) } } } Spacer(minLength: 0) } Text(notification.message) .font(.subheadline) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) if compactLayout { VStack(alignment: .leading, spacing: 10) { timestamp if notification.isUnread { markReadButton } } } else { HStack { timestamp Spacer(minLength: 0) if notification.isUnread { markReadButton } } } } .padding(compactLayout ? 18 : 20) .appSurface(radius: 24) } private var timestamp: some View { Text(notification.sentAt.formatted(date: .abbreviated, time: .shortened)) .font(.footnote) .foregroundStyle(.secondary) } private var markReadButton: some View { Button { onMarkRead() } label: { Label("Mark read", systemImage: "checkmark") } .buttonStyle(.bordered) } private var accentColor: Color { switch notification.kind { case .approval: .green case .security: .orange case .system: .blue } } } struct NotificationBellButton: View { @ObservedObject var model: AppViewModel var body: some View { Button { model.isNotificationCenterPresented = true } label: { Image(systemName: imageName) .font(.headline) .foregroundStyle(iconTone) .frame(width: 28, height: 28, alignment: .center) .background(alignment: .center) { #if os(iOS) GeometryReader { proxy in Color.clear .preference(key: NotificationBellFrameKey.self, value: proxy.frame(in: .global)) } #endif } } .accessibilityLabel("Notifications") } private var imageName: String { #if os(iOS) model.unreadNotificationCount == 0 ? "bell" : "bell.fill" #else model.unreadNotificationCount == 0 ? "bell" : "bell.badge.fill" #endif } private var iconTone: some ShapeStyle { model.unreadNotificationCount == 0 ? Color.primary : dashboardAccent } } struct NotificationCenterSheet: View { @ObservedObject var model: AppViewModel @Environment(\.dismiss) private var dismiss @Environment(\.horizontalSizeClass) private var horizontalSizeClass var body: some View { NavigationStack { AppScrollScreen( compactLayout: compactLayout, bottomPadding: compactLayout ? AppLayout.compactBottomDockPadding : AppLayout.regularBottomPadding ) { NotificationsPanel(model: model, compactLayout: compactLayout) } .navigationTitle("Notifications") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Done") { dismiss() } } } } #if os(iOS) .presentationDetents(compactLayout ? [.large] : [.medium, .large]) #endif } private var compactLayout: Bool { #if os(iOS) horizontalSizeClass == .compact #else false #endif } }