Files
swiftapp/Sources/Features/Home/HomeRootView.swift

3140 lines
115 KiB
Swift

import SwiftUI
private let dashboardAccent = Color(red: 0.12, green: 0.40, blue: 0.31)
private let dashboardGold = Color(red: 0.84, green: 0.71, blue: 0.48)
private let dashboardBorder = Color.black.opacity(0.06)
private let dashboardShadow = Color.black.opacity(0.05)
private enum DashboardSpacing {
static let compactOuterPadding: CGFloat = 16
static let regularOuterPadding: CGFloat = 28
static let compactTopPadding: CGFloat = 10
static let regularTopPadding: CGFloat = 18
static let compactBottomPadding: CGFloat = 120
static let regularBottomPadding: CGFloat = 56
static let compactStackSpacing: CGFloat = 20
static let regularStackSpacing: CGFloat = 28
static let compactContentWidth: CGFloat = 720
static let regularContentWidth: CGFloat = 980
static let compactSectionPadding: CGFloat = 18
static let regularSectionPadding: CGFloat = 24
static let compactRadius: CGFloat = 24
static let regularRadius: CGFloat = 28
}
private extension View {
func dashboardSurface(radius: CGFloat, fillOpacity: Double = 0.88) -> some View {
background(
Color.white.opacity(fillOpacity),
in: RoundedRectangle(cornerRadius: radius, style: .continuous)
)
.overlay(
RoundedRectangle(cornerRadius: radius, style: .continuous)
.stroke(dashboardBorder, lineWidth: 1)
)
.shadow(color: dashboardShadow, radius: 14, y: 6)
}
}
struct HomeRootView: View {
@ObservedObject var model: AppViewModel
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
var body: some View {
ZStack {
DashboardBackdrop()
Group {
if usesCompactNavigation {
CompactHomeContainer(model: model)
} else {
RegularHomeContainer(model: model)
}
}
}
.sheet(isPresented: $model.isNotificationCenterPresented) {
NotificationCenterSheet(model: model)
}
}
private var usesCompactNavigation: Bool {
#if os(iOS)
horizontalSizeClass == .compact
#else
false
#endif
}
}
private struct CompactHomeContainer: View {
@ObservedObject var model: AppViewModel
var body: some View {
TabView(selection: $model.selectedSection) {
compactTab(for: .overview)
compactTab(for: .requests)
compactTab(for: .activity)
compactTab(for: .account)
}
}
@ViewBuilder
private func compactTab(for section: AppSection) -> some View {
NavigationStack {
HomeSectionScreen(model: model, section: section, compactLayout: true)
.navigationTitle(section.title)
.toolbar {
DashboardToolbar(model: model, compactLayout: true)
}
}
.tag(section)
.tabItem {
Label(section.title, systemImage: section.systemImage)
}
}
}
private struct RegularHomeContainer: View {
@ObservedObject var model: AppViewModel
var body: some View {
NavigationSplitView {
Sidebar(model: model)
} detail: {
HomeSectionScreen(model: model, section: model.selectedSection, compactLayout: false)
.navigationTitle(model.selectedSection.title)
.toolbar {
DashboardToolbar(model: model, compactLayout: false)
}
}
.navigationSplitViewStyle(.balanced)
}
}
private struct DashboardToolbar: ToolbarContent {
@ObservedObject var model: AppViewModel
let compactLayout: Bool
var body: some ToolbarContent {
if compactLayout {
ToolbarItemGroup(placement: .primaryAction) {
NotificationBellButton(model: model)
Menu {
Button {
Task {
await model.refreshDashboard()
}
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
}
Button {
Task {
await model.simulateIncomingRequest()
}
} label: {
Label("Mock Request", systemImage: "sparkles.rectangle.stack.fill")
}
Button {
Task {
await model.sendTestNotification()
}
} label: {
Label("Send Test Alert", systemImage: "paperplane.fill")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
} else {
ToolbarItemGroup(placement: .primaryAction) {
NotificationBellButton(model: model)
Button {
Task {
await model.refreshDashboard()
}
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
}
.disabled(model.isRefreshing)
Button {
Task {
await model.simulateIncomingRequest()
}
} label: {
Label("Mock Request", systemImage: "sparkles.rectangle.stack.fill")
}
Button {
Task {
await model.sendTestNotification()
}
} label: {
Label("Test Alert", systemImage: "paperplane.fill")
}
}
}
}
}
private struct HomeSectionScreen: View {
@ObservedObject var model: AppViewModel
let section: AppSection
let compactLayout: Bool
@State private var focusedRequest: ApprovalRequest?
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: compactLayout ? DashboardSpacing.compactStackSpacing : DashboardSpacing.regularStackSpacing) {
if let banner = model.bannerMessage {
BannerCard(message: banner, compactLayout: compactLayout)
}
switch section {
case .overview:
OverviewPanel(
model: model,
compactLayout: compactLayout,
onOpenRequest: { focusedRequest = $0 }
)
case .requests:
RequestsPanel(
model: model,
compactLayout: compactLayout,
onOpenRequest: { focusedRequest = $0 }
)
case .activity:
ActivityPanel(
model: model,
compactLayout: compactLayout,
onOpenRequest: { focusedRequest = $0 }
)
case .account:
AccountPanel(model: model, compactLayout: compactLayout)
}
}
.padding(.horizontal, compactLayout ? DashboardSpacing.compactOuterPadding : DashboardSpacing.regularOuterPadding)
.padding(.top, compactLayout ? DashboardSpacing.compactTopPadding : DashboardSpacing.regularTopPadding)
.padding(.bottom, compactLayout ? DashboardSpacing.compactBottomPadding : DashboardSpacing.regularBottomPadding)
.frame(maxWidth: compactLayout ? DashboardSpacing.compactContentWidth : DashboardSpacing.regularContentWidth, alignment: .leading)
.frame(maxWidth: .infinity, alignment: compactLayout ? .leading : .center)
}
.scrollIndicators(.hidden)
.sheet(item: $focusedRequest) { request in
RequestDetailSheet(request: request, model: model)
}
}
}
private struct Sidebar: View {
@ObservedObject var model: AppViewModel
var body: some View {
List {
Section {
SidebarStatusCard(
profile: model.profile,
pendingCount: model.pendingRequests.count,
unreadCount: model.unreadNotificationCount
)
}
Section("Workspace") {
ForEach(AppSection.allCases) { section in
sidebarRow(for: section)
}
}
}
.navigationTitle("idp.global")
}
private func badgeCount(for section: AppSection) -> Int {
switch section {
case .overview:
0
case .requests:
model.pendingRequests.count
case .activity:
0
case .account:
0
}
}
@ViewBuilder
private func sidebarRow(for section: AppSection) -> some View {
Button {
model.selectedSection = section
} label: {
HStack(spacing: 14) {
Image(systemName: section.systemImage)
.font(.headline)
.frame(width: 30, height: 30)
.background {
if model.selectedSection == section {
Circle()
.fill(dashboardAccent.opacity(0.18))
} else {
Circle()
.fill(.thinMaterial)
}
}
.foregroundStyle(model.selectedSection == section ? dashboardAccent : .primary)
Text(section.title)
.font(.headline)
Spacer()
if badgeCount(for: section) > 0 {
Text("\(badgeCount(for: section))")
.font(.caption.weight(.semibold))
.padding(.horizontal, 9)
.padding(.vertical, 5)
.background(.thinMaterial, in: Capsule())
}
}
.padding(.vertical, 4)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.listRowBackground(
model.selectedSection == section
? dashboardAccent.opacity(0.12)
: Color.clear
)
}
}
private struct SidebarStatusCard: View {
let profile: MemberProfile?
let pendingCount: Int
let unreadCount: Int
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Digital Passport")
.font(.title3.weight(.semibold))
Text(profile?.handle ?? "Not paired yet")
.foregroundStyle(.secondary)
HStack(spacing: 10) {
SmallMetricPill(title: "Pending", value: "\(pendingCount)")
SmallMetricPill(title: "Unread", value: "\(unreadCount)")
}
}
.padding(.vertical, 6)
}
}
private struct OverviewPanel: View {
@ObservedObject var model: AppViewModel
let compactLayout: Bool
let onOpenRequest: (ApprovalRequest) -> Void
var body: some View {
VStack(alignment: .leading, spacing: compactLayout ? 18 : 24) {
if let profile = model.profile, let session = model.session {
OverviewHero(
profile: profile,
session: session,
pendingCount: model.pendingRequests.count,
unreadCount: model.unreadNotificationCount,
compactLayout: compactLayout
)
}
SectionCard(
title: "Quick Actions",
subtitle: "Refresh the bound session, seed a request, or test device alerts while the backend is still mocked.",
compactLayout: compactLayout
) {
QuickActionsDeck(model: model, compactLayout: compactLayout)
}
SectionCard(
title: "Requests In Focus",
subtitle: "Your passport is the identity surface. This queue is where anything asking for access should earn trust.",
compactLayout: compactLayout
) {
if model.pendingRequests.isEmpty {
EmptyStateCopy(
title: "Nothing waiting",
systemImage: "checkmark.shield.fill",
message: "Every pending approval has been handled."
)
} else {
VStack(spacing: 16) {
if let featured = model.pendingRequests.first {
FeaturedRequestCard(
request: featured,
compactLayout: compactLayout,
onOpenRequest: { onOpenRequest(featured) }
)
}
ForEach(model.pendingRequests.dropFirst().prefix(2)) { request in
RequestCard(
request: request,
compactLayout: compactLayout,
isBusy: model.activeRequestID == request.id,
onApprove: {
Task { await model.approve(request) }
},
onReject: {
Task { await model.reject(request) }
},
onOpenRequest: {
onOpenRequest(request)
}
)
}
}
}
}
SectionCard(
title: "Recent Activity",
subtitle: "Keep the full timeline in its own view, and use the bell above for alerts that need device-level attention.",
compactLayout: compactLayout
) {
ActivityPreviewCard(model: model, compactLayout: compactLayout)
}
}
}
}
private struct ActivityPanel: View {
@ObservedObject var model: AppViewModel
let compactLayout: Bool
let onOpenRequest: (ApprovalRequest) -> Void
@State private var selectedNotificationID: AppNotification.ID?
private var notificationIDs: [AppNotification.ID] {
model.notifications.map(\.id)
}
private var selectedNotification: AppNotification? {
if let selectedNotificationID,
let match = model.notifications.first(where: { $0.id == selectedNotificationID }) {
return match
}
return model.notifications.first
}
var body: some View {
VStack(alignment: .leading, spacing: compactLayout ? 18 : 24) {
if compactLayout {
SectionCard(
title: "Recent Activity",
subtitle: "A dedicated home for approvals, pairing events, and system changes after they happen."
) {
VStack(spacing: 16) {
activityMetricRow
if model.notifications.isEmpty {
EmptyStateCopy(
title: "No activity yet",
systemImage: "clock.badge.xmark",
message: "Once requests and pairing events arrive, the timeline will fill in here."
)
} else {
ForEach(model.notifications) { notification in
NotificationCard(
notification: notification,
compactLayout: compactLayout,
onMarkRead: {
Task { await model.markNotificationRead(notification) }
}
)
}
}
}
}
} else {
SectionCard(
title: "Activity Timeline",
subtitle: "Review what already happened across approvals, pairing, and system state without mixing it into the notification surface."
) {
VStack(alignment: .leading, spacing: 18) {
activityMetricRow
if model.notifications.isEmpty {
EmptyStateCopy(
title: "No activity yet",
systemImage: "clock.badge.xmark",
message: "Once requests and pairing events arrive, the timeline will fill in here."
)
} else {
HStack(alignment: .top, spacing: 18) {
VStack(alignment: .leading, spacing: 14) {
Text("Timeline")
.font(.headline)
Text("The latest product and security events stay readable here, while the bell above stays focused on device notifications.")
.foregroundStyle(.secondary)
VStack(spacing: 12) {
ForEach(model.notifications) { notification in
NotificationFeedRow(
notification: notification,
isSelected: notification.id == selectedNotification?.id
) {
selectedNotificationID = notification.id
}
}
}
}
.frame(width: 390, alignment: .leading)
.padding(18)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 28, style: .continuous))
if let notification = selectedNotification {
NotificationWorkbenchDetail(
notification: notification,
permissionState: model.notificationPermission,
onMarkRead: {
Task { await model.markNotificationRead(notification) }
}
)
}
}
}
}
}
}
if !model.handledRequests.isEmpty {
SectionCard(
title: "Handled Requests",
subtitle: "A compact audit trail for the approvals and rejections that already moved through the queue."
) {
LazyVStack(spacing: 14) {
ForEach(model.handledRequests.prefix(compactLayout ? 4 : 6)) { request in
RequestCard(
request: request,
compactLayout: compactLayout,
isBusy: false,
onApprove: nil,
onReject: nil,
onOpenRequest: {
onOpenRequest(request)
}
)
}
}
}
}
}
.onChange(of: notificationIDs, initial: true) { _, _ in
syncSelectedNotification()
}
}
@ViewBuilder
private var activityMetricRow: some View {
if compactLayout {
VStack(spacing: 10) {
SmallMetricPill(title: "Events", value: "\(model.notifications.count)")
SmallMetricPill(title: "Unread", value: "\(model.unreadNotificationCount)")
SmallMetricPill(title: "Handled", value: "\(model.handledRequests.count)")
}
} else {
HStack(spacing: 14) {
NotificationMetricCard(
title: "Events",
value: "\(model.notifications.count)",
subtitle: model.notifications.isEmpty ? "Quiet so far" : "Timeline active",
accent: dashboardAccent
)
NotificationMetricCard(
title: "Unread",
value: "\(model.unreadNotificationCount)",
subtitle: model.unreadNotificationCount == 0 ? "Everything acknowledged" : "Still highlighted",
accent: .orange
)
NotificationMetricCard(
title: "Handled",
value: "\(model.handledRequests.count)",
subtitle: model.handledRequests.isEmpty ? "No completed approvals yet" : "Recent decisions ready to review",
accent: dashboardGold
)
}
}
}
private func syncSelectedNotification() {
if let selectedNotificationID,
notificationIDs.contains(selectedNotificationID) {
return
}
selectedNotificationID = model.notifications.first?.id
}
}
private struct RequestsPanel: View {
@ObservedObject var model: AppViewModel
let compactLayout: Bool
let onOpenRequest: (ApprovalRequest) -> Void
@State private var selectedRequestID: ApprovalRequest.ID?
private var requestIDs: [ApprovalRequest.ID] {
model.requests.map(\.id)
}
private var selectedRequest: ApprovalRequest? {
if let selectedRequestID,
let match = model.requests.first(where: { $0.id == selectedRequestID }) {
return match
}
return model.pendingRequests.first ?? model.handledRequests.first
}
var body: some View {
VStack(alignment: .leading, spacing: compactLayout ? 18 : 24) {
if compactLayout {
SectionCard(
title: "Approval Desk",
subtitle: "Treat every request like a border checkpoint: verify the origin, timing, and scope before letting it through.",
compactLayout: compactLayout
) {
VStack(spacing: 16) {
RequestQueueSummary(
pendingCount: model.pendingRequests.count,
elevatedCount: model.elevatedPendingCount,
compactLayout: compactLayout
)
if model.pendingRequests.isEmpty {
EmptyStateCopy(
title: "Queue is clear",
systemImage: "checkmark.circle",
message: "Use the toolbar to simulate another request if you want to keep testing."
)
} else {
ForEach(model.pendingRequests) { request in
RequestCard(
request: request,
compactLayout: compactLayout,
isBusy: model.activeRequestID == request.id,
onApprove: {
Task { await model.approve(request) }
},
onReject: {
Task { await model.reject(request) }
},
onOpenRequest: {
onOpenRequest(request)
}
)
}
}
}
}
SectionCard(
title: "Decision Guide",
subtitle: "What to check before approving high-sensitivity actions from your phone.",
compactLayout: compactLayout
) {
VStack(alignment: .leading, spacing: 14) {
GuidanceRow(
icon: "network.badge.shield.half.filled",
title: "Confirm the origin",
message: "The service hostname should match the product or automation you intentionally triggered."
)
GuidanceRow(
icon: "timer",
title: "Look for short lifetimes",
message: "Privileged grants should usually be limited in time instead of creating long-lived access."
)
GuidanceRow(
icon: "lock.shield",
title: "Escalate mentally for elevated scopes",
message: "Signing, publishing, and write scopes deserve a slower second look before approval."
)
}
}
if !model.handledRequests.isEmpty {
SectionCard(
title: "Recently Handled",
subtitle: "A compact audit trail of the latest approvals and rejections.",
compactLayout: compactLayout
) {
LazyVStack(spacing: 14) {
ForEach(model.handledRequests.prefix(4)) { request in
RequestCard(
request: request,
compactLayout: compactLayout,
isBusy: false,
onApprove: nil,
onReject: nil,
onOpenRequest: {
onOpenRequest(request)
}
)
}
}
}
}
} else {
SectionCard(
title: "Approval Workbench",
subtitle: "Use the queue on the left and a richer inline review on the right so each decision feels deliberate instead of mechanical."
) {
VStack(alignment: .leading, spacing: 18) {
RequestQueueSummary(
pendingCount: model.pendingRequests.count,
elevatedCount: model.elevatedPendingCount,
compactLayout: compactLayout
)
if model.requests.isEmpty {
EmptyStateCopy(
title: "Queue is clear",
systemImage: "checkmark.circle",
message: "Use the toolbar to simulate another request if you want to keep testing."
)
} else {
HStack(alignment: .top, spacing: 18) {
VStack(alignment: .leading, spacing: 14) {
Text("Queue")
.font(.headline)
Text("Pending and recently handled items stay visible here so you can sanity-check decisions without leaving the flow.")
.foregroundStyle(.secondary)
VStack(spacing: 12) {
ForEach(model.requests) { request in
RequestQueueRow(
request: request,
isSelected: request.id == selectedRequest?.id
) {
selectedRequestID = request.id
}
}
}
}
.frame(width: 390, alignment: .leading)
.padding(18)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 28, style: .continuous))
if let request = selectedRequest {
RequestWorkbenchDetail(
request: request,
isBusy: model.activeRequestID == request.id,
onApprove: request.status == .pending ? {
Task { await model.approve(request) }
} : nil,
onReject: request.status == .pending ? {
Task { await model.reject(request) }
} : nil,
onOpenRequest: {
onOpenRequest(request)
}
)
}
}
}
}
}
SectionCard(
title: "Operator Checklist",
subtitle: "A calm review pattern for larger screens, especially when elevated scopes show up."
) {
LazyVGrid(
columns: [
GridItem(.flexible(), spacing: 14),
GridItem(.flexible(), spacing: 14)
],
alignment: .leading,
spacing: 14
) {
GuidanceCard(
icon: "network.badge.shield.half.filled",
title: "Confirm the origin",
message: "The hostname should map to the workflow or portal you intentionally triggered."
)
GuidanceCard(
icon: "timer",
title: "Look for short lifetimes",
message: "Elevated grants are safer when they expire quickly instead of becoming ambient access."
)
GuidanceCard(
icon: "lock.shield",
title: "Escalate for signing and publish scopes",
message: "If the action can sign, publish, or write, slow down and verify the target system twice."
)
GuidanceCard(
icon: "person.badge.shield.checkmark",
title: "Match the device",
message: "The request story should line up with the paired browser, CLI, or automation session you expect."
)
}
}
}
}
.onChange(of: requestIDs, initial: true) { _, _ in
syncSelectedRequest()
}
}
private func syncSelectedRequest() {
if let selectedRequestID,
requestIDs.contains(selectedRequestID) {
return
}
selectedRequestID = model.pendingRequests.first?.id ?? model.handledRequests.first?.id
}
}
private struct NotificationsPanel: View {
@ObservedObject var model: AppViewModel
let compactLayout: Bool
@State private var selectedNotificationID: AppNotification.ID?
private var notificationIDs: [AppNotification.ID] {
model.notifications.map(\.id)
}
private var selectedNotification: AppNotification? {
if let selectedNotificationID,
let match = model.notifications.first(where: { $0.id == selectedNotificationID }) {
return match
}
return model.notifications.first
}
var body: some View {
VStack(alignment: .leading, spacing: compactLayout ? 18 : 24) {
if compactLayout {
SectionCard(
title: "Notification Delivery",
subtitle: "Control lock-screen delivery now, then evolve this into remote push once the backend is live.",
compactLayout: compactLayout
) {
NotificationPermissionCard(model: model, compactLayout: compactLayout)
}
SectionCard(
title: "Alert Inbox",
subtitle: "Unread alerts stay emphasized here until you explicitly clear them.",
compactLayout: compactLayout
) {
if model.notifications.isEmpty {
EmptyStateCopy(
title: "No alerts yet",
systemImage: "bell.slash",
message: "New pairing and approval alerts will accumulate here."
)
} else {
LazyVStack(spacing: 14) {
ForEach(model.notifications) { notification in
NotificationCard(
notification: notification,
compactLayout: compactLayout,
onMarkRead: {
Task { await model.markNotificationRead(notification) }
}
)
}
}
}
}
} else {
SectionCard(
title: "Delivery Posture",
subtitle: "Keep delivery health, unread pressure, and the latest alert in one glance from the notification center."
) {
VStack(alignment: .leading, spacing: 18) {
HStack(spacing: 14) {
NotificationMetricCard(
title: "Unread",
value: "\(model.unreadNotificationCount)",
subtitle: model.unreadNotificationCount == 0 ? "Inbox clear" : "Needs triage",
accent: .orange
)
NotificationMetricCard(
title: "Permission",
value: model.notificationPermission.title,
subtitle: model.notificationPermission == .allowed ? "Lock screen ready" : "Review device status",
accent: dashboardAccent
)
NotificationMetricCard(
title: "Latest",
value: model.latestNotification?.kind.title ?? "Quiet",
subtitle: model.latestNotification?.sentAt.formatted(date: .omitted, time: .shortened) ?? "No recent events",
accent: dashboardGold
)
}
NotificationPermissionCard(model: model, compactLayout: compactLayout)
}
}
SectionCard(
title: "Alert Inbox",
subtitle: "Select an alert to inspect the message body, delivery state, and the right follow-up action."
) {
if model.notifications.isEmpty {
EmptyStateCopy(
title: "No alerts yet",
systemImage: "bell.slash",
message: "New pairing and approval alerts will accumulate here."
)
} else {
HStack(alignment: .top, spacing: 18) {
VStack(alignment: .leading, spacing: 14) {
Text("Feed")
.font(.headline)
Text("Unread items stay visually lifted until you clear them, which makes it easier to scan the important changes first.")
.foregroundStyle(.secondary)
VStack(spacing: 12) {
ForEach(model.notifications) { notification in
NotificationFeedRow(
notification: notification,
isSelected: notification.id == selectedNotification?.id
) {
selectedNotificationID = notification.id
}
}
}
}
.frame(maxWidth: 340, alignment: .leading)
.padding(18)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 28, style: .continuous))
if let notification = selectedNotification {
NotificationWorkbenchDetail(
notification: notification,
permissionState: model.notificationPermission,
onMarkRead: {
Task { await model.markNotificationRead(notification) }
}
)
}
}
}
}
}
}
.onChange(of: notificationIDs, initial: true) { _, _ in
syncSelectedNotification()
}
}
private func syncSelectedNotification() {
if let selectedNotificationID,
notificationIDs.contains(selectedNotificationID) {
return
}
selectedNotificationID = model.notifications.first?.id
}
}
private struct AccountPanel: View {
@ObservedObject var model: AppViewModel
let compactLayout: Bool
var body: some View {
VStack(alignment: .leading, spacing: compactLayout ? 18 : 24) {
if let profile = model.profile, let session = model.session {
AccountHero(profile: profile, session: session, compactLayout: compactLayout)
SectionCard(
title: "Session Security",
subtitle: "The core trust facts for the currently paired session.",
compactLayout: compactLayout
) {
AccountFactGrid(profile: profile, session: session, compactLayout: compactLayout)
}
}
SectionCard(
title: "Mock Pairing Payload",
subtitle: "Useful for testing QR flow while the real portal integration is still pending.",
compactLayout: compactLayout
) {
Text(model.suggestedQRCodePayload)
.font(.body.monospaced())
.textSelection(.enabled)
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 24, style: .continuous))
}
SectionCard(
title: "Session Controls",
subtitle: "Use this once you want to reset back to the login and pairing flow.",
compactLayout: compactLayout
) {
Button(role: .destructive) {
model.signOut()
} label: {
Label("Sign Out", systemImage: "rectangle.portrait.and.arrow.right")
}
.buttonStyle(.bordered)
}
}
}
}
private struct OverviewHero: View {
let profile: MemberProfile
let session: AuthSession
let pendingCount: Int
let unreadCount: Int
let compactLayout: Bool
var body: some View {
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 34, style: .continuous)
.fill(
LinearGradient(
colors: [
Color(red: 0.07, green: 0.18, blue: 0.15),
Color(red: 0.11, green: 0.28, blue: 0.24),
Color(red: 0.29, green: 0.24, blue: 0.12)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.overlay(
RoundedRectangle(cornerRadius: 34, style: .continuous)
.strokeBorder(dashboardGold.opacity(0.55), lineWidth: 1.2)
)
Circle()
.fill(.white.opacity(0.08))
.frame(width: compactLayout ? 180 : 260, height: compactLayout ? 180 : 260)
.offset(x: compactLayout ? 210 : 420, y: compactLayout ? -30 : -50)
Image(systemName: "globe.europe.africa.fill")
.font(.system(size: compactLayout ? 92 : 122))
.foregroundStyle(.white.opacity(0.07))
.offset(x: compactLayout ? 220 : 455, y: compactLayout ? 4 : 8)
VStack(alignment: .leading, spacing: compactLayout ? 16 : 20) {
passportHeader
passportBody
if !compactLayout {
PassportMachineStrip(code: machineReadableCode)
}
passportMetrics
}
.padding(compactLayout ? 22 : 28)
}
.frame(minHeight: compactLayout ? 380 : 390)
}
private var passportHeader: some View {
HStack(alignment: .top, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text("IDP.GLOBAL DIGITAL PASSPORT")
.font(.caption.weight(.bold))
.tracking(1.8)
.foregroundStyle(.white.opacity(0.78))
Text(profile.name)
.font(.system(size: compactLayout ? 30 : 36, weight: .bold, design: .rounded))
.foregroundStyle(.white)
Text("Bound to \(session.deviceName) for requests coming from \(session.originHost).")
.font(compactLayout ? .subheadline : .title3)
.foregroundStyle(.white.opacity(0.88))
}
Spacer(minLength: 0)
PassportDocumentBadge(
number: documentNumber,
issuedAt: session.pairedAt,
compactLayout: compactLayout
)
}
}
@ViewBuilder
private var passportBody: some View {
if compactLayout {
VStack(alignment: .leading, spacing: 14) {
HStack(alignment: .top, spacing: 14) {
passportPortrait
VStack(alignment: .leading, spacing: 10) {
PassportField(label: "Holder", value: profile.name, emphasized: true)
PassportField(label: "Handle", value: profile.handle, monospaced: true)
PassportField(label: "Origin", value: session.originHost, monospaced: true)
}
}
LazyVGrid(
columns: [
GridItem(.flexible(), spacing: 10),
GridItem(.flexible(), spacing: 10)
],
spacing: 10
) {
PassportInlineFact(label: "Device", value: session.deviceName)
PassportInlineFact(label: "Issued", value: session.pairedAt.formatted(date: .abbreviated, time: .shortened))
PassportInlineFact(label: "Organization", value: profile.organization)
PassportInlineFact(label: "Token", value: "...\(session.tokenPreview)", monospaced: true)
}
}
.padding(18)
.background(.white.opacity(0.11), in: RoundedRectangle(cornerRadius: 28, style: .continuous))
} else {
VStack(alignment: .leading, spacing: 16) {
HStack(alignment: .top, spacing: 18) {
passportPortrait
HStack(alignment: .top, spacing: 14) {
passportPrimaryFields
passportSecondaryFields
}
}
LazyVGrid(
columns: [
GridItem(.flexible(), spacing: 12),
GridItem(.flexible(), spacing: 12),
GridItem(.flexible(), spacing: 12)
],
spacing: 12
) {
PassportInlineFact(label: "Document No.", value: documentNumber, monospaced: true)
PassportInlineFact(label: "Issued", value: session.pairedAt.formatted(date: .abbreviated, time: .shortened))
PassportInlineFact(label: "Membership", value: "\(profile.deviceCount) trusted devices")
}
}
.padding(20)
.background(.white.opacity(0.11), in: RoundedRectangle(cornerRadius: 28, style: .continuous))
}
}
private var passportMetrics: some View {
Group {
if compactLayout {
VStack(spacing: 10) {
passportMetricCards
}
} else {
HStack(spacing: 12) {
passportMetricCards
}
}
}
}
@ViewBuilder
private var passportMetricCards: some View {
PassportMetricBadge(
title: "Pending",
value: "\(pendingCount)",
subtitle: pendingCount == 0 ? "No approvals waiting" : "Requests still at the border"
)
PassportMetricBadge(
title: "Alerts",
value: "\(unreadCount)",
subtitle: unreadCount == 0 ? "Notification bell is clear" : "Unread device alerts"
)
PassportMetricBadge(
title: "Devices",
value: "\(profile.deviceCount)",
subtitle: "\(profile.organization) membership"
)
}
private var passportPortrait: some View {
VStack(alignment: .leading, spacing: 12) {
RoundedRectangle(cornerRadius: 26, style: .continuous)
.fill(.white.opacity(0.12))
.frame(width: compactLayout ? 102 : 132, height: compactLayout ? 132 : 166)
.overlay {
VStack(spacing: 10) {
Circle()
.fill(.white.opacity(0.18))
.frame(width: compactLayout ? 52 : 64, height: compactLayout ? 52 : 64)
.overlay {
Text(holderInitials)
.font(.system(size: compactLayout ? 24 : 28, weight: .bold, design: .rounded))
.foregroundStyle(.white)
}
Text("TRUSTED HOLDER")
.font(.caption2.weight(.bold))
.tracking(1.2)
.foregroundStyle(.white.opacity(0.72))
Text(compactLayout ? documentNumber : profile.handle)
.font(.footnote.monospaced())
.foregroundStyle(.white.opacity(0.9))
.lineLimit(2)
.minimumScaleFactor(0.7)
}
.padding(12)
}
Text("Issued \(session.pairedAt.formatted(date: .abbreviated, time: .shortened))")
.font(.caption)
.foregroundStyle(.white.opacity(0.74))
}
}
private var passportPrimaryFields: some View {
VStack(alignment: .leading, spacing: 12) {
PassportField(label: "Holder", value: profile.name, emphasized: true)
PassportField(label: "Handle", value: profile.handle, monospaced: true)
PassportField(label: "Organization", value: profile.organization)
}
}
private var passportSecondaryFields: some View {
VStack(alignment: .leading, spacing: 12) {
PassportField(label: "Bound Device", value: session.deviceName)
PassportField(label: "Origin", value: session.originHost, monospaced: true)
PassportField(label: "Token Preview", value: "...\(session.tokenPreview)", monospaced: true)
}
}
private var holderInitials: String {
let parts = profile.name
.split(separator: " ")
.prefix(2)
.compactMap { $0.first }
let initials = String(parts)
return initials.isEmpty ? "ID" : initials.uppercased()
}
private var documentNumber: String {
"IDP-\(session.id.uuidString.prefix(8).uppercased())"
}
private var machineReadableCode: String {
let normalizedName = sanitize(profile.name)
let normalizedHandle = sanitize(profile.handle)
let normalizedOrigin = sanitize(session.originHost)
return "P<\(documentNumber)<\(normalizedName)<<\(normalizedHandle)<<\(normalizedOrigin)"
}
private func sanitize(_ value: String) -> String {
value
.uppercased()
.map { character in
character.isLetter || character.isNumber ? String(character) : "<"
}
.joined()
}
}
private struct PassportDocumentBadge: View {
let number: String
let issuedAt: Date
let compactLayout: Bool
var body: some View {
VStack(alignment: .trailing, spacing: 8) {
StatusBadge(title: "Bound", tone: .white)
VStack(alignment: .trailing, spacing: 4) {
Text("Document No.")
.font(.caption2.weight(.bold))
.tracking(1.0)
.foregroundStyle(.white.opacity(0.72))
Text(number)
.font((compactLayout ? Font.footnote : Font.body).monospaced().weight(.semibold))
.foregroundStyle(.white)
}
if !compactLayout {
Text("Issued \(issuedAt.formatted(date: .abbreviated, time: .shortened))")
.font(.caption)
.foregroundStyle(.white.opacity(0.76))
}
}
.padding(.horizontal, compactLayout ? 12 : 14)
.padding(.vertical, 10)
.background(.white.opacity(0.10), in: RoundedRectangle(cornerRadius: 22, style: .continuous))
}
}
private struct PassportInlineFact: View {
let label: String
let value: String
var monospaced: Bool = false
var body: some View {
VStack(alignment: .leading, spacing: 5) {
Text(label.uppercased())
.font(.caption2.weight(.bold))
.tracking(1.0)
.foregroundStyle(.white.opacity(0.72))
Text(value)
.font(monospaced ? .subheadline.monospaced() : .subheadline.weight(.semibold))
.foregroundStyle(.white)
.lineLimit(2)
.minimumScaleFactor(0.7)
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.white.opacity(0.09), in: RoundedRectangle(cornerRadius: 18, style: .continuous))
}
}
private struct PassportField: View {
let label: String
let value: String
var monospaced: Bool = false
var emphasized: Bool = false
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(label.uppercased())
.font(.caption2.weight(.bold))
.tracking(1.0)
.foregroundStyle(.white.opacity(0.72))
Text(value)
.font(valueFont)
.foregroundStyle(.white)
.lineLimit(2)
.minimumScaleFactor(0.8)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var valueFont: Font {
if monospaced {
return .body.monospaced()
}
return emphasized ? .headline : .body
}
}
private struct PassportMetricBadge: View {
let title: String
let value: String
let subtitle: String
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title.uppercased())
.font(.caption.weight(.bold))
.tracking(1.0)
.foregroundStyle(.white.opacity(0.72))
Text(value)
.font(.title2.weight(.bold))
.foregroundStyle(.white)
Text(subtitle)
.font(.footnote)
.foregroundStyle(.white.opacity(0.82))
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.white.opacity(0.10), in: RoundedRectangle(cornerRadius: 22, style: .continuous))
}
}
private struct PassportMachineStrip: View {
let code: String
var body: some View {
Text(code)
.font(.caption.monospaced().weight(.semibold))
.lineLimit(1)
.minimumScaleFactor(0.5)
.padding(.horizontal, 14)
.padding(.vertical, 12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.black.opacity(0.22), in: RoundedRectangle(cornerRadius: 18, style: .continuous))
.foregroundStyle(.white.opacity(0.94))
}
}
private struct QuickActionsDeck: View {
@ObservedObject var model: AppViewModel
let compactLayout: Bool
var body: some View {
Group {
if compactLayout {
VStack(spacing: 12) {
actionButtons
}
} else {
HStack(alignment: .top, spacing: 14) {
actionButtons
}
}
}
}
@ViewBuilder
private var actionButtons: some View {
ActionTile(
title: "Refresh State",
subtitle: "Pull the latest requests and notifications from the mock service.",
systemImage: "arrow.clockwise"
) {
Task {
await model.refreshDashboard()
}
}
ActionTile(
title: "Seed Request",
subtitle: "Inject a new elevated approval flow to test the queue.",
systemImage: "sparkles.rectangle.stack.fill"
) {
Task {
await model.simulateIncomingRequest()
}
}
ActionTile(
title: "Test Alert",
subtitle: "Schedule a local notification so the phone behavior is easy to verify.",
systemImage: "bell.badge.fill"
) {
Task {
await model.sendTestNotification()
}
}
}
}
private struct ActionTile: View {
let title: String
let subtitle: String
let systemImage: String
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(alignment: .leading, spacing: 12) {
Image(systemName: systemImage)
.font(.title3.weight(.semibold))
.foregroundStyle(dashboardAccent)
.frame(width: 42, height: 42)
.background(dashboardAccent.opacity(0.10), in: RoundedRectangle(cornerRadius: 14, style: .continuous))
Text(title)
.font(.headline)
.foregroundStyle(.primary)
Text(subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(18)
.background(Color.white.opacity(0.76), in: RoundedRectangle(cornerRadius: 24, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 24, style: .continuous)
.stroke(dashboardAccent.opacity(0.08), lineWidth: 1)
)
}
.buttonStyle(.plain)
}
}
private struct FeaturedRequestCard: View {
let request: ApprovalRequest
let compactLayout: Bool
let onOpenRequest: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack(alignment: .center, spacing: 12) {
Image(systemName: request.risk == .elevated ? "shield.lefthalf.filled.badge.checkmark" : request.kind.systemImage)
.font(.title2)
.foregroundStyle(request.risk == .elevated ? .orange : dashboardAccent)
VStack(alignment: .leading, spacing: 4) {
Text(request.trustHeadline)
.font(.headline)
Text(request.title)
.font(.title3.weight(.semibold))
}
Spacer()
StatusBadge(
title: request.risk.title,
tone: request.risk == .routine ? .mint : .orange
)
}
Text(request.trustDetail)
.foregroundStyle(.secondary)
HStack(spacing: 8) {
StatusBadge(title: request.kind.title, tone: .blue)
StatusBadge(title: request.source, tone: .gray)
StatusBadge(title: request.scopeSummary, tone: .green)
}
if compactLayout {
VStack(alignment: .leading, spacing: 12) {
Button("Review Full Context", action: onOpenRequest)
.buttonStyle(.borderedProminent)
Text(request.risk.guidance)
.font(.footnote)
.foregroundStyle(.secondary)
}
} else {
HStack {
Button("Review Full Context", action: onOpenRequest)
.buttonStyle(.borderedProminent)
Spacer()
Text(request.risk.guidance)
.font(.footnote)
.foregroundStyle(.secondary)
.multilineTextAlignment(.trailing)
}
}
}
.padding(compactLayout ? 18 : 22)
.background(
LinearGradient(
colors: [
request.risk == .routine ? dashboardAccent.opacity(0.12) : Color.orange.opacity(0.16),
Color.white.opacity(0.7)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
in: RoundedRectangle(cornerRadius: 28, style: .continuous)
)
}
}
private struct RequestQueueSummary: View {
let pendingCount: Int
let elevatedCount: Int
let compactLayout: Bool
var body: some View {
if compactLayout {
VStack(spacing: 12) {
HStack(spacing: 12) {
pendingCard
elevatedCard
}
postureCard
}
} else {
HStack(spacing: 12) {
pendingCard
elevatedCard
postureCard
}
}
}
private var pendingCard: some View {
RequestSummaryMetricCard(
title: "Pending",
value: "\(pendingCount)",
subtitle: pendingCount == 0 ? "Queue is clear" : "Still waiting on your call",
accent: dashboardAccent
)
}
private var elevatedCard: some View {
RequestSummaryMetricCard(
title: "Elevated",
value: "\(elevatedCount)",
subtitle: elevatedCount == 0 ? "No privileged scopes" : "Needs slower review",
accent: .orange
)
}
private var postureCard: some View {
RequestSummaryMetricCard(
title: "Posture",
value: trustMode,
subtitle: postureSummary,
accent: dashboardGold
)
}
private var trustMode: String {
if pendingCount == 0 {
return "Clear"
}
if elevatedCount == 0 {
return "Active"
}
return elevatedCount > 1 ? "Escalate" : "Guarded"
}
private var postureSummary: String {
if pendingCount == 0 {
return "Nothing at the border"
}
if elevatedCount == 0 {
return "Routine traffic only"
}
return "Privileged access in queue"
}
}
private struct RequestSummaryMetricCard: View {
let title: String
let value: String
let subtitle: String
let accent: Color
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title.uppercased())
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
Text(value)
.font(.title3.weight(.semibold))
.foregroundStyle(.primary)
Text(subtitle)
.font(.footnote)
.foregroundStyle(.secondary)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(accent.opacity(0.10), in: RoundedRectangle(cornerRadius: 20, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.stroke(accent.opacity(0.08), lineWidth: 1)
)
}
}
private struct NotificationPermissionCard: View {
@ObservedObject var model: AppViewModel
let compactLayout: Bool
var body: some View {
VStack(alignment: .leading, spacing: 18) {
HStack(alignment: .top, spacing: 14) {
Image(systemName: model.notificationPermission.systemImage)
.font(.title2)
.frame(width: 38, height: 38)
.background(.thinMaterial, in: Circle())
.foregroundStyle(dashboardAccent)
VStack(alignment: .leading, spacing: 5) {
Text(model.notificationPermission.title)
.font(.headline)
Text(model.notificationPermission.summary)
.foregroundStyle(.secondary)
}
}
Group {
if compactLayout {
VStack(spacing: 12) {
permissionButtons
}
} else {
HStack(spacing: 12) {
permissionButtons
}
}
}
}
.padding(18)
.dashboardSurface(radius: 24)
}
@ViewBuilder
private var permissionButtons: some View {
Button {
Task {
await model.requestNotificationAccess()
}
} label: {
Label("Enable Notifications", systemImage: "bell.and.waves.left.and.right.fill")
}
.buttonStyle(.borderedProminent)
Button {
Task {
await model.sendTestNotification()
}
} label: {
Label("Send Test Alert", systemImage: "paperplane.fill")
}
.buttonStyle(.bordered)
}
}
private struct ActivityPreviewCard: View {
@ObservedObject var model: AppViewModel
let compactLayout: Bool
var body: some View {
VStack(alignment: .leading, spacing: 16) {
if let latest = model.latestNotification {
NotificationCard(
notification: latest,
compactLayout: compactLayout,
onMarkRead: {
Task { await model.markNotificationRead(latest) }
}
)
} else {
EmptyStateCopy(
title: "No activity yet",
systemImage: "clock.badge.xmark",
message: "Once requests and pairing events arrive, the activity timeline will fill in here."
)
}
if compactLayout {
VStack(alignment: .leading, spacing: 12) {
Button {
model.selectedSection = .activity
} label: {
Label("Open Activity", systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90")
}
.buttonStyle(.borderedProminent)
Button {
model.isNotificationCenterPresented = true
} label: {
Label("Open Notification Bell", systemImage: "bell")
}
.buttonStyle(.bordered)
}
} else {
HStack(spacing: 12) {
Button {
model.selectedSection = .activity
} label: {
Label("Open Activity", systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90")
}
.buttonStyle(.borderedProminent)
Button {
model.isNotificationCenterPresented = true
} label: {
Label("Open Notifications", systemImage: "bell")
}
.buttonStyle(.bordered)
Spacer()
Text("Unread device alerts now live in the bell above instead of taking a full navigation slot.")
.font(.footnote)
.foregroundStyle(.secondary)
.multilineTextAlignment(.trailing)
}
}
}
}
}
private struct NotificationBellButton: View {
@ObservedObject var model: AppViewModel
var body: some View {
Button {
model.isNotificationCenterPresented = true
} label: {
ZStack(alignment: .topTrailing) {
Image(systemName: model.unreadNotificationCount == 0 ? "bell" : "bell.badge.fill")
.font(.headline)
.foregroundStyle(model.unreadNotificationCount == 0 ? .primary : dashboardAccent)
if model.unreadNotificationCount > 0 {
Text("\(min(model.unreadNotificationCount, 9))")
.font(.caption2.weight(.bold))
.padding(.horizontal, 5)
.padding(.vertical, 2)
.background(Color.orange, in: Capsule())
.foregroundStyle(.white)
.offset(x: 10, y: -10)
}
}
.frame(width: 28, height: 28)
}
.accessibilityLabel("Notifications")
}
}
private struct NotificationCenterSheet: View {
@ObservedObject var model: AppViewModel
@Environment(\.dismiss) private var dismiss
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
var body: some View {
NavigationStack {
ScrollView {
NotificationsPanel(model: model, compactLayout: compactLayout)
.padding(.horizontal, compactLayout ? DashboardSpacing.compactOuterPadding : DashboardSpacing.regularOuterPadding)
.padding(.top, compactLayout ? DashboardSpacing.compactTopPadding : DashboardSpacing.regularTopPadding)
.padding(.bottom, compactLayout ? DashboardSpacing.compactBottomPadding : DashboardSpacing.regularBottomPadding)
.frame(maxWidth: compactLayout ? DashboardSpacing.compactContentWidth : DashboardSpacing.regularContentWidth, alignment: .leading)
.frame(maxWidth: .infinity, alignment: compactLayout ? .leading : .center)
}
.scrollIndicators(.hidden)
.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
}
}
private struct AccountHero: View {
let profile: MemberProfile
let session: AuthSession
let compactLayout: Bool
var body: some View {
ZStack(alignment: .bottomLeading) {
RoundedRectangle(cornerRadius: 32, style: .continuous)
.fill(
LinearGradient(
colors: [
dashboardAccent.opacity(0.95),
Color(red: 0.19, green: 0.49, blue: 0.40),
dashboardGold.opacity(0.92)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
VStack(alignment: .leading, spacing: 14) {
Text(profile.name)
.font(.system(size: compactLayout ? 28 : 34, weight: .bold, design: .rounded))
.foregroundStyle(.white)
Text(profile.handle)
.font(.headline)
.foregroundStyle(.white.opacity(0.84))
Text("Current trusted device: \(session.deviceName)")
.foregroundStyle(.white.opacity(0.86))
}
.padding(compactLayout ? 22 : 28)
}
.frame(minHeight: compactLayout ? 190 : 220)
}
}
private struct AccountFactGrid: View {
let profile: MemberProfile
let session: AuthSession
let compactLayout: Bool
private var columns: [GridItem] {
Array(repeating: GridItem(.flexible(), spacing: 12), count: compactLayout ? 1 : 2)
}
var body: some View {
LazyVGrid(columns: columns, spacing: 12) {
FactCard(label: "Organization", value: profile.organization)
FactCard(label: "Origin", value: session.originHost)
FactCard(label: "Paired At", value: session.pairedAt.formatted(date: .abbreviated, time: .shortened))
FactCard(label: "Token Preview", value: "\(session.tokenPreview)")
FactCard(label: "Trusted Devices", value: "\(profile.deviceCount)")
FactCard(label: "Recovery", value: profile.recoverySummary)
}
}
}
private struct RequestCard: View {
let request: ApprovalRequest
let compactLayout: Bool
let isBusy: Bool
let onApprove: (() -> Void)?
let onReject: (() -> Void)?
let onOpenRequest: (() -> Void)?
private var infoColumns: [GridItem] {
Array(repeating: GridItem(.flexible(), spacing: 10), count: compactLayout ? 2 : 3)
}
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack(alignment: .top, spacing: 14) {
ZStack {
Circle()
.fill(requestAccent.opacity(0.14))
Image(systemName: request.kind.systemImage)
.font(.title2)
.foregroundStyle(requestAccent)
}
.frame(width: 46, height: 46)
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(request.trustHeadline)
.font(.subheadline.weight(.semibold))
.foregroundStyle(requestAccent)
Text(request.title)
.font(.headline)
.foregroundStyle(.primary)
}
Spacer()
StatusBadge(
title: request.status.title,
tone: statusTone
)
}
Text(request.subtitle)
.foregroundStyle(.secondary)
HStack(spacing: 8) {
StatusBadge(title: request.kind.title, tone: .blue)
StatusBadge(title: request.risk.title, tone: request.risk == .routine ? .mint : .orange)
Text(request.createdAt, style: .relative)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
LazyVGrid(columns: infoColumns, alignment: .leading, spacing: 10) {
RequestFactPill(label: "Source", value: request.source, accent: dashboardAccent)
RequestFactPill(
label: "Requested",
value: request.createdAt.formatted(date: .abbreviated, time: .shortened),
accent: dashboardGold
)
RequestFactPill(label: "Access", value: request.scopeSummary, accent: requestAccent)
}
VStack(alignment: .leading, spacing: 10) {
Label(request.status == .pending ? "Decision posture" : "Decision record", systemImage: request.status.systemImage)
.font(.headline)
.foregroundStyle(.primary)
Text(request.trustDetail)
.foregroundStyle(.secondary)
Text(reviewSummary)
.font(.subheadline.weight(.semibold))
.foregroundStyle(requestAccent)
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(requestAccent.opacity(0.10), in: RoundedRectangle(cornerRadius: 22, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 22, style: .continuous)
.stroke(requestAccent.opacity(0.08), lineWidth: 1)
)
if !request.scopes.isEmpty {
VStack(alignment: .leading, spacing: 10) {
Text("Requested scopes")
.font(.subheadline.weight(.semibold))
FlowScopes(scopes: request.scopes)
}
}
VStack(spacing: 12) {
if let onOpenRequest {
Button {
onOpenRequest()
} label: {
Label("Review Details", systemImage: "arrow.up.forward.app")
}
.buttonStyle(.bordered)
.frame(maxWidth: .infinity, alignment: .leading)
}
if let onApprove, let onReject, request.status == .pending {
if compactLayout {
VStack(spacing: 10) {
Button {
onApprove()
} label: {
if isBusy {
ProgressView()
} else {
Label("Approve Request", systemImage: "checkmark.circle.fill")
}
}
.buttonStyle(.borderedProminent)
.disabled(isBusy)
Button(role: .destructive) {
onReject()
} label: {
Label("Reject Request", systemImage: "xmark.circle.fill")
}
.buttonStyle(.bordered)
.disabled(isBusy)
}
} else {
HStack(spacing: 12) {
Button {
onApprove()
} label: {
if isBusy {
ProgressView()
} else {
Label("Approve", systemImage: "checkmark.circle.fill")
}
}
.buttonStyle(.borderedProminent)
.disabled(isBusy)
Button(role: .destructive) {
onReject()
} label: {
Label("Reject", systemImage: "xmark.circle.fill")
}
.buttonStyle(.bordered)
.disabled(isBusy)
}
}
}
}
}
.padding(compactLayout ? 18 : 20)
.background(
LinearGradient(
colors: [
Color.white.opacity(0.92),
requestAccent.opacity(0.05)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
in: RoundedRectangle(cornerRadius: 28, style: .continuous)
)
.overlay(
RoundedRectangle(cornerRadius: 28, style: .continuous)
.stroke(requestAccent.opacity(0.10), lineWidth: 1)
)
.shadow(color: dashboardShadow, radius: 12, y: 5)
}
private var statusTone: Color {
switch request.status {
case .pending:
return .orange
case .approved:
return .green
case .rejected:
return .red
}
}
private var requestAccent: Color {
switch request.status {
case .approved:
return .green
case .rejected:
return .red
case .pending:
return request.risk == .routine ? dashboardAccent : .orange
}
}
private var reviewSummary: String {
switch request.status {
case .pending:
if request.risk == .elevated {
return "This is privileged access. Let it through only if the origin and the moment both match what you just initiated."
}
return "This looks routine, but it still needs to match the browser, CLI, or device session you expect."
case .approved:
return "This request was already approved in the mock queue and is now part of the recent audit trail."
case .rejected:
return "This request was rejected and should remain a closed lane unless a new request is issued."
}
}
}
private struct RequestQueueRow: View {
let request: ApprovalRequest
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(alignment: .top, spacing: 12) {
ZStack {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(rowAccent.opacity(0.14))
Image(systemName: request.kind.systemImage)
.font(.headline)
.foregroundStyle(rowAccent)
}
.frame(width: 38, height: 38)
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(request.title)
.font(.headline)
.foregroundStyle(.primary)
.multilineTextAlignment(.leading)
.lineLimit(2)
Text(request.trustHeadline)
.font(.subheadline.weight(.semibold))
.foregroundStyle(rowAccent)
.lineLimit(1)
}
Spacer(minLength: 0)
StatusBadge(
title: request.status.title,
tone: statusTone
)
}
Text(request.source)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
Text(request.subtitle)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(1)
.multilineTextAlignment(.leading)
HStack(spacing: 8) {
StatusBadge(title: request.risk.title, tone: request.risk == .routine ? .mint : .orange)
StatusBadge(title: request.scopeSummary, tone: .blue)
Spacer()
Text(request.createdAt, style: .relative)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
Image(systemName: isSelected ? "chevron.right.circle.fill" : "chevron.right")
.font(.headline)
.foregroundStyle(isSelected ? rowAccent : .secondary.opacity(0.7))
.padding(.top, 2)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(backgroundStyle, in: RoundedRectangle(cornerRadius: 24, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 24, style: .continuous)
.stroke(isSelected ? rowAccent.opacity(0.36) : Color.clear, lineWidth: 1.5)
)
.overlay(alignment: .leading) {
Capsule()
.fill(rowAccent.opacity(isSelected ? 0.80 : 0.30))
.frame(width: 5)
.padding(.vertical, 16)
.padding(.leading, 8)
}
}
.buttonStyle(.plain)
}
private var statusTone: Color {
switch request.status {
case .pending:
.orange
case .approved:
.green
case .rejected:
.red
}
}
private var backgroundStyle: Color {
isSelected ? rowAccent.opacity(0.08) : Color.white.opacity(0.90)
}
private var rowAccent: Color {
switch request.status {
case .approved:
.green
case .rejected:
.red
case .pending:
request.risk == .routine ? dashboardAccent : .orange
}
}
}
private struct RequestWorkbenchDetail: View {
let request: ApprovalRequest
let isBusy: Bool
let onApprove: (() -> Void)?
let onReject: (() -> Void)?
let onOpenRequest: () -> Void
private let columns = [
GridItem(.flexible(), spacing: 12),
GridItem(.flexible(), spacing: 12)
]
var body: some View {
VStack(alignment: .leading, spacing: 18) {
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.fill(
LinearGradient(
colors: [
request.risk == .routine ? dashboardAccent.opacity(0.95) : Color.orange.opacity(0.92),
dashboardGold.opacity(0.88)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.overlay(
RoundedRectangle(cornerRadius: 30, style: .continuous)
.strokeBorder(requestAccent.opacity(0.20), lineWidth: 1)
)
VStack(alignment: .leading, spacing: 16) {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 8) {
StatusBadge(title: request.kind.title, tone: .white)
StatusBadge(title: request.risk.title, tone: .white)
StatusBadge(title: request.status.title, tone: .white)
}
Text(request.title)
.font(.system(size: 30, weight: .bold, design: .rounded))
.foregroundStyle(.white)
Text(request.trustHeadline)
.font(.headline)
.foregroundStyle(.white.opacity(0.84))
}
Spacer(minLength: 0)
VStack(alignment: .trailing, spacing: 6) {
Text("REQUESTED")
.font(.caption.weight(.bold))
.foregroundStyle(.white.opacity(0.72))
Text(request.createdAt.formatted(date: .abbreviated, time: .shortened))
.font(.subheadline.weight(.semibold))
.foregroundStyle(.white)
}
}
Text(request.subtitle)
.font(.title3)
.foregroundStyle(.white.opacity(0.88))
HStack(spacing: 14) {
Label(request.source, systemImage: "network")
Label(request.scopeSummary, systemImage: "lock.shield")
}
.font(.subheadline)
.foregroundStyle(.white.opacity(0.88))
Text(request.trustDetail)
.font(.subheadline)
.foregroundStyle(.white.opacity(0.82))
}
.padding(24)
}
.frame(minHeight: 220)
LazyVGrid(columns: columns, spacing: 12) {
FactCard(label: "Source", value: request.source)
FactCard(label: "Requested", value: request.createdAt.formatted(date: .abbreviated, time: .shortened))
FactCard(label: "Type", value: request.kind.title)
FactCard(label: "Status", value: request.status.title)
FactCard(label: "Risk", value: request.risk.summary)
FactCard(label: "Access", value: request.scopeSummary)
}
HStack(alignment: .top, spacing: 12) {
RequestSignalCard(
title: "Trust Signals",
subtitle: "The approval story should match the device, the product, and the moment you just triggered.",
accent: requestAccent
) {
VStack(alignment: .leading, spacing: 14) {
GuidanceRow(
icon: "network.badge.shield.half.filled",
title: "Source must look familiar",
message: "This request comes from \(request.source). Only approve if that host or product lines up with what you intended."
)
GuidanceRow(
icon: "person.badge.shield.checkmark",
title: "Action should fit the session",
message: request.trustDetail
)
GuidanceRow(
icon: request.risk == .routine ? "checkmark.shield" : "exclamationmark.shield",
title: request.risk == .routine ? "Routine review is still a review" : "Elevated access deserves a pause",
message: request.risk.guidance
)
}
}
RequestSignalCard(
title: "Access Envelope",
subtitle: "These are the capabilities this request wants before it can proceed.",
accent: dashboardGold
) {
if request.scopes.isEmpty {
Text("The mock backend did not provide explicit scopes for this request.")
.foregroundStyle(.secondary)
} else {
FlowScopes(scopes: request.scopes)
}
}
}
RequestSignalCard(
title: request.status == .pending ? "Decision Rail" : "Decision Record",
subtitle: request.status == .pending
? "Use the actions below only once the request story matches the device in your hand."
: "This request already moved through the queue, so this rail becomes a compact audit note.",
accent: statusTone
) {
VStack(alignment: .leading, spacing: 14) {
Text(request.trustDetail)
.foregroundStyle(.secondary)
Text(decisionSummary)
.font(.headline)
HStack(spacing: 12) {
Button {
onOpenRequest()
} label: {
Label("Open Full Review", systemImage: "arrow.up.forward.app")
}
.buttonStyle(.bordered)
Spacer()
if let onApprove, let onReject, request.status == .pending {
Button {
onApprove()
} label: {
if isBusy {
ProgressView()
} else {
Label("Approve", systemImage: "checkmark.circle.fill")
}
}
.buttonStyle(.borderedProminent)
.disabled(isBusy)
Button(role: .destructive) {
onReject()
} label: {
Label("Reject", systemImage: "xmark.circle.fill")
}
.buttonStyle(.bordered)
.disabled(isBusy)
}
}
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var statusTone: Color {
switch request.status {
case .pending:
return .orange
case .approved:
return .green
case .rejected:
return .red
}
}
private var requestAccent: Color {
request.risk == .routine ? dashboardAccent : .orange
}
private var decisionSummary: String {
switch request.status {
case .pending:
return request.risk == .routine
? "Approve only if the origin and timing feel boringly expected."
: "Privileged requests should feel unmistakably intentional before you approve them."
case .approved:
return "This request has already been approved and should now be treated as part of your recent decision history."
case .rejected:
return "This request was rejected and is now a record of a blocked access attempt."
}
}
}
private struct RequestFactPill: View {
let label: String
let value: String
let accent: Color
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(label.uppercased())
.font(.caption2.weight(.semibold))
.foregroundStyle(.secondary)
Text(value)
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
.lineLimit(2)
.minimumScaleFactor(0.8)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(accent.opacity(0.10), in: RoundedRectangle(cornerRadius: 18, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 18, style: .continuous)
.stroke(accent.opacity(0.08), lineWidth: 1)
)
}
}
private struct RequestSignalCard<Content: View>: View {
let title: String
let subtitle: String
let accent: Color
let content: () -> Content
init(
title: String,
subtitle: String,
accent: Color,
@ViewBuilder content: @escaping () -> Content
) {
self.title = title
self.subtitle = subtitle
self.accent = accent
self.content = content
}
var body: some View {
VStack(alignment: .leading, spacing: 14) {
HStack(alignment: .top, spacing: 12) {
Circle()
.fill(accent.opacity(0.16))
.frame(width: 34, height: 34)
.overlay {
Circle()
.stroke(accent.opacity(0.30), lineWidth: 1)
}
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
Text(subtitle)
.foregroundStyle(.secondary)
}
}
content()
}
.padding(18)
.frame(maxWidth: .infinity, alignment: .leading)
.dashboardSurface(radius: 24)
}
}
private struct RequestDetailSheet: View {
let request: ApprovalRequest
@ObservedObject var model: AppViewModel
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
RequestDetailHero(request: request)
SectionCard(
title: "Requested Access",
subtitle: "The exact scopes or capabilities this action wants to receive."
) {
if request.scopes.isEmpty {
Text("No explicit scopes were provided by the mock backend.")
.foregroundStyle(.secondary)
} else {
FlowScopes(scopes: request.scopes)
}
}
SectionCard(
title: "Trust Signals",
subtitle: "The details to validate before you approve anything sensitive."
) {
VStack(alignment: .leading, spacing: 12) {
FactCard(label: "Source", value: request.source)
FactCard(label: "Requested", value: request.createdAt.formatted(date: .abbreviated, time: .shortened))
FactCard(label: "Type", value: request.kind.title)
FactCard(label: "Risk", value: request.risk.summary)
}
}
SectionCard(
title: "Decision Guidance",
subtitle: "A short operator-minded reminder before you accept or reject this request."
) {
Text(request.trustDetail)
.foregroundStyle(.secondary)
Text(request.risk.guidance)
.font(.headline)
}
if request.status == .pending {
VStack(spacing: 12) {
Button {
Task {
await model.approve(request)
dismiss()
}
} label: {
if model.activeRequestID == request.id {
ProgressView()
} else {
Label("Approve Request", systemImage: "checkmark.circle.fill")
}
}
.buttonStyle(.borderedProminent)
.disabled(model.activeRequestID == request.id)
Button(role: .destructive) {
Task {
await model.reject(request)
dismiss()
}
} label: {
Label("Reject Request", systemImage: "xmark.circle.fill")
}
.buttonStyle(.bordered)
.disabled(model.activeRequestID == request.id)
}
}
}
.padding(.horizontal, DashboardSpacing.compactOuterPadding)
.padding(.top, DashboardSpacing.compactTopPadding)
.padding(.bottom, DashboardSpacing.compactBottomPadding)
.frame(maxWidth: DashboardSpacing.compactContentWidth, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .leading)
}
.navigationTitle("Review Request")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Close") {
dismiss()
}
}
}
}
}
}
private struct RequestDetailHero: View {
let request: ApprovalRequest
var body: some View {
ZStack(alignment: .bottomLeading) {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.fill(
LinearGradient(
colors: [
request.risk == .routine ? dashboardAccent.opacity(0.92) : Color.orange.opacity(0.92),
dashboardGold.opacity(0.88)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
VStack(alignment: .leading, spacing: 12) {
Text(request.trustHeadline)
.font(.headline)
.foregroundStyle(.white.opacity(0.86))
Text(request.title)
.font(.system(size: 30, weight: .bold, design: .rounded))
.foregroundStyle(.white)
Text(request.subtitle)
.foregroundStyle(.white.opacity(0.86))
}
.padding(24)
}
.frame(minHeight: 210)
}
}
private struct NotificationCard: View {
let notification: AppNotification
let compactLayout: Bool
let onMarkRead: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 14) {
HStack(alignment: .top, spacing: 14) {
Image(systemName: notification.kind.systemImage)
.font(.title3)
.frame(width: 38, height: 38)
.background(.thinMaterial, in: Circle())
.foregroundStyle(accentColor)
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(notification.title)
.font(.headline)
Spacer()
if notification.isUnread {
StatusBadge(title: "Unread", tone: .orange)
}
}
Text(notification.kind.summary)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
Text(notification.message)
.foregroundStyle(.secondary)
Group {
if compactLayout {
VStack(alignment: .leading, spacing: 10) {
timestampLabel
if notification.isUnread {
markReadButton
}
}
} else {
HStack {
timestampLabel
Spacer()
if notification.isUnread {
markReadButton
}
}
}
}
}
.padding(compactLayout ? 16 : 18)
.dashboardSurface(radius: compactLayout ? 22 : 24)
}
private var timestampLabel: 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
}
}
}
private struct NotificationMetricCard: View {
let title: String
let value: String
let subtitle: String
let accent: Color
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(title.uppercased())
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
Text(value)
.font(.title3.weight(.semibold))
.foregroundStyle(.primary)
Text(subtitle)
.font(.footnote)
.foregroundStyle(.secondary)
}
.padding(18)
.frame(maxWidth: .infinity, alignment: .leading)
.background(accent.opacity(0.10), in: RoundedRectangle(cornerRadius: 20, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.stroke(accent.opacity(0.08), lineWidth: 1)
)
}
}
private struct NotificationFeedRow: View {
let notification: AppNotification
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .top, spacing: 12) {
Image(systemName: notification.kind.systemImage)
.font(.headline)
.foregroundStyle(accentColor)
.frame(width: 34, height: 34)
.background(.thinMaterial, in: Circle())
VStack(alignment: .leading, spacing: 4) {
Text(notification.title)
.font(.headline)
.foregroundStyle(.primary)
.multilineTextAlignment(.leading)
Text(notification.kind.summary)
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer(minLength: 0)
if notification.isUnread {
Circle()
.fill(Color.orange)
.frame(width: 10, height: 10)
}
}
Text(notification.message)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(2)
.multilineTextAlignment(.leading)
HStack {
StatusBadge(title: notification.kind.title, tone: accentColor)
Spacer()
Text(notification.sentAt.formatted(date: .omitted, time: .shortened))
.font(.footnote)
.foregroundStyle(.secondary)
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(backgroundStyle, in: RoundedRectangle(cornerRadius: 24, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 24, style: .continuous)
.stroke(isSelected ? accentColor.opacity(0.35) : Color.clear, lineWidth: 1.5)
)
}
.buttonStyle(.plain)
}
private var accentColor: Color {
switch notification.kind {
case .approval:
.green
case .security:
.orange
case .system:
.blue
}
}
private var backgroundStyle: Color {
isSelected ? accentColor.opacity(0.10) : Color.white.opacity(0.58)
}
}
private struct NotificationWorkbenchDetail: View {
let notification: AppNotification
let permissionState: NotificationPermissionState
let onMarkRead: () -> Void
private let columns = [
GridItem(.flexible(), spacing: 12),
GridItem(.flexible(), spacing: 12)
]
var body: some View {
VStack(alignment: .leading, spacing: 18) {
ZStack(alignment: .bottomLeading) {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.fill(
LinearGradient(
colors: [
accentColor.opacity(0.95),
accentColor.opacity(0.70),
dashboardGold.opacity(0.82)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 8) {
StatusBadge(title: notification.kind.title, tone: .white)
StatusBadge(title: notification.isUnread ? "Unread" : "Read", tone: .white)
}
Text(notification.title)
.font(.system(size: 30, weight: .bold, design: .rounded))
.foregroundStyle(.white)
Text(notification.message)
.foregroundStyle(.white.opacity(0.9))
}
.padding(24)
}
.frame(minHeight: 210)
LazyVGrid(columns: columns, spacing: 12) {
FactCard(label: "Category", value: notification.kind.summary)
FactCard(label: "Sent", value: notification.sentAt.formatted(date: .abbreviated, time: .shortened))
FactCard(label: "Inbox State", value: notification.isUnread ? "Still highlighted" : "Already cleared")
FactCard(label: "Delivery", value: permissionState.title)
}
VStack(alignment: .leading, spacing: 10) {
Text("Delivery Context")
.font(.headline)
Text(permissionState.summary)
.foregroundStyle(.secondary)
Text(notification.isUnread ? "This alert is still asking for attention in the in-app feed." : "This alert has already been acknowledged in the mock inbox.")
.font(.headline)
}
.padding(18)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 26, style: .continuous))
if notification.isUnread {
Button {
onMarkRead()
} label: {
Label("Mark Read", systemImage: "checkmark")
}
.buttonStyle(.borderedProminent)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var accentColor: Color {
switch notification.kind {
case .approval:
.green
case .security:
.orange
case .system:
.blue
}
}
}
private struct SectionCard<Content: View>: View {
let title: String
let subtitle: String
let compactLayout: Bool
let content: () -> Content
init(
title: String,
subtitle: String,
compactLayout: Bool = false,
@ViewBuilder content: @escaping () -> Content
) {
self.title = title
self.subtitle = subtitle
self.compactLayout = compactLayout
self.content = content
}
var body: some View {
VStack(alignment: .leading, spacing: 18) {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.title2.weight(.semibold))
Text(subtitle)
.foregroundStyle(.secondary)
}
content()
}
.padding(compactLayout ? DashboardSpacing.compactSectionPadding : DashboardSpacing.regularSectionPadding)
.frame(maxWidth: .infinity, alignment: .leading)
.dashboardSurface(radius: compactLayout ? DashboardSpacing.compactRadius : DashboardSpacing.regularRadius)
}
}
private struct BannerCard: View {
let message: String
let compactLayout: Bool
var body: some View {
HStack(spacing: 12) {
Image(systemName: "sparkles")
.font(.title3)
.foregroundStyle(dashboardAccent)
Text(message)
.font(compactLayout ? .subheadline.weight(.semibold) : .headline)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.dashboardSurface(radius: 999, fillOpacity: 0.84)
}
}
private struct SmallMetricPill: View {
let title: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(title.uppercased())
.font(.caption2.weight(.semibold))
.foregroundStyle(.secondary)
Text(value)
.font(.headline)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 18, style: .continuous))
}
}
private struct HeroMetric: View {
let title: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title.uppercased())
.font(.caption.weight(.semibold))
.foregroundStyle(.white.opacity(0.72))
Text(value)
.font(.title2.weight(.bold))
.foregroundStyle(.white)
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.white.opacity(0.12), in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
private struct GuidanceRow: View {
let icon: String
let title: String
let message: String
var body: some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: icon)
.font(.title3)
.frame(width: 32)
.foregroundStyle(dashboardAccent)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
Text(message)
.foregroundStyle(.secondary)
}
}
}
}
private struct GuidanceCard: View {
let icon: String
let title: String
let message: String
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Image(systemName: icon)
.font(.title3)
.foregroundStyle(dashboardAccent)
Text(title)
.font(.headline)
Text(message)
.foregroundStyle(.secondary)
}
.padding(18)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 24, style: .continuous))
}
}
private struct FlowScopes: View {
let scopes: [String]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(scopes, id: \.self) { scope in
Text(scope)
.font(.caption.monospaced())
.padding(.horizontal, 10)
.padding(.vertical, 8)
.background(.thinMaterial, in: Capsule())
}
}
}
}
}
private struct EmptyStateCopy: View {
let title: String
let systemImage: String
let message: String
var body: some View {
ContentUnavailableView(
title,
systemImage: systemImage,
description: Text(message)
)
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
}
}
private struct FactCard: View {
let label: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(label.uppercased())
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
Text(value)
.font(.body)
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.dashboardSurface(radius: 18)
}
}
private struct StatusBadge: View {
let title: String
let tone: Color
var body: some View {
Text(title)
.font(.caption.weight(.semibold))
.lineLimit(1)
.minimumScaleFactor(0.8)
.fixedSize(horizontal: true, vertical: false)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(tone.opacity(0.14), in: Capsule())
.foregroundStyle(tone)
}
}
private struct DashboardBackdrop: View {
var body: some View {
LinearGradient(
colors: [
Color(red: 0.98, green: 0.98, blue: 0.97),
Color.white,
Color(red: 0.97, green: 0.98, blue: 0.99)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.overlay(alignment: .topLeading) {
Circle()
.fill(dashboardAccent.opacity(0.10))
.frame(width: 360, height: 360)
.blur(radius: 70)
.offset(x: -120, y: -120)
}
.overlay(alignment: .bottomTrailing) {
Circle()
.fill(dashboardGold.opacity(0.12))
.frame(width: 420, height: 420)
.blur(radius: 90)
.offset(x: 140, y: 160)
}
.ignoresSafeArea()
}
}