2026-04-17 22:08:27 +02:00
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 )
2026-04-17 22:29:16 +02:00
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 )
}
}
2026-04-17 22:08:27 +02:00
struct HomeRootView : View {
@ ObservedObject var model : AppViewModel
@ Environment ( \ . horizontalSizeClass ) private var horizontalSizeClass
var body : some View {
2026-04-17 22:29:16 +02:00
ZStack {
DashboardBackdrop ( )
Group {
if usesCompactNavigation {
CompactHomeContainer ( model : model )
} else {
RegularHomeContainer ( model : model )
}
2026-04-17 22:08:27 +02:00
}
}
. 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 {
2026-04-17 22:29:16 +02:00
VStack ( alignment : . leading , spacing : compactLayout ? DashboardSpacing . compactStackSpacing : DashboardSpacing . regularStackSpacing ) {
2026-04-17 22:08:27 +02:00
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 )
}
}
2026-04-17 22:29:16 +02:00
. 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 )
2026-04-17 22:08:27 +02:00
}
. 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
}
}
}
}
2026-04-17 22:29:16 +02:00
. frame ( width : 390 , alignment : . leading )
2026-04-17 22:08:27 +02:00
. 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
}
}
}
}
2026-04-17 22:29:16 +02:00
. frame ( width : 390 , alignment : . leading )
2026-04-17 22:08:27 +02:00
. 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 ) {
2026-04-17 22:29:16 +02:00
passportHeader
2026-04-17 22:08:27 +02:00
2026-04-17 22:29:16 +02:00
passportBody
2026-04-17 22:08:27 +02:00
2026-04-17 22:29:16 +02:00
if ! compactLayout {
PassportMachineStrip ( code : machineReadableCode )
2026-04-17 22:08:27 +02:00
}
2026-04-17 22:29:16 +02:00
passportMetrics
}
. padding ( compactLayout ? 22 : 28 )
}
. frame ( minHeight : compactLayout ? 380 : 390 )
}
2026-04-17 22:08:27 +02:00
2026-04-17 22:29:16 +02:00
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 ) )
2026-04-17 22:08:27 +02:00
2026-04-17 22:29:16 +02:00
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 )
2026-04-17 22:08:27 +02:00
}
}
2026-04-17 22:29:16 +02:00
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
2026-04-17 22:08:27 +02:00
2026-04-17 22:29:16 +02:00
HStack ( alignment : . top , spacing : 14 ) {
passportPrimaryFields
passportSecondaryFields
2026-04-17 22:08:27 +02:00
}
}
2026-04-17 22:29:16 +02:00
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
}
2026-04-17 22:08:27 +02:00
}
}
2026-04-17 22:29:16 +02:00
}
@ 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 "
)
2026-04-17 22:08:27 +02:00
}
private var passportPortrait : some View {
VStack ( alignment : . leading , spacing : 12 ) {
RoundedRectangle ( cornerRadius : 26 , style : . continuous )
. fill ( . white . opacity ( 0.12 ) )
2026-04-17 22:29:16 +02:00
. frame ( width : compactLayout ? 102 : 132 , height : compactLayout ? 132 : 166 )
2026-04-17 22:08:27 +02:00
. overlay {
VStack ( spacing : 10 ) {
Circle ( )
. fill ( . white . opacity ( 0.18 ) )
2026-04-17 22:29:16 +02:00
. frame ( width : compactLayout ? 52 : 64 , height : compactLayout ? 52 : 64 )
2026-04-17 22:08:27 +02:00
. 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 ) )
2026-04-17 22:29:16 +02:00
Text ( compactLayout ? documentNumber : profile . handle )
2026-04-17 22:08:27 +02:00
. font ( . footnote . monospaced ( ) )
. foregroundStyle ( . white . opacity ( 0.9 ) )
2026-04-17 22:29:16 +02:00
. lineLimit ( 2 )
. minimumScaleFactor ( 0.7 )
2026-04-17 22:08:27 +02:00
}
. 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 ( )
}
2026-04-17 22:29:16 +02:00
private var documentNumber : String {
" IDP- \( session . id . uuidString . prefix ( 8 ) . uppercased ( ) ) "
}
2026-04-17 22:08:27 +02:00
private var machineReadableCode : String {
let normalizedName = sanitize ( profile . name )
let normalizedHandle = sanitize ( profile . handle )
2026-04-17 22:29:16 +02:00
let normalizedOrigin = sanitize ( session . originHost )
return " P< \( documentNumber ) < \( normalizedName ) << \( normalizedHandle ) << \( normalizedOrigin ) "
2026-04-17 22:08:27 +02:00
}
private func sanitize ( _ value : String ) -> String {
value
. uppercased ( )
. map { character in
character . isLetter || character . isNumber ? String ( character ) : " < "
}
. joined ( )
}
}
2026-04-17 22:29:16 +02:00
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 ) )
}
}
2026-04-17 22:08:27 +02:00
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 )
2026-04-17 22:29:16 +02:00
. font ( . title3 . weight ( . semibold ) )
2026-04-17 22:08:27 +02:00
. foregroundStyle ( dashboardAccent )
2026-04-17 22:29:16 +02:00
. frame ( width : 42 , height : 42 )
. background ( dashboardAccent . opacity ( 0.10 ) , in : RoundedRectangle ( cornerRadius : 14 , style : . continuous ) )
2026-04-17 22:08:27 +02:00
Text ( title )
. font ( . headline )
. foregroundStyle ( . primary )
Text ( subtitle )
. font ( . subheadline )
. foregroundStyle ( . secondary )
}
. frame ( maxWidth : . infinity , alignment : . leading )
. padding ( 18 )
2026-04-17 22:29:16 +02:00
. 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 )
)
2026-04-17 22:08:27 +02:00
}
. 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 {
2026-04-17 22:29:16 +02:00
if compactLayout {
VStack ( spacing : 12 ) {
2026-04-17 22:08:27 +02:00
HStack ( spacing : 12 ) {
2026-04-17 22:29:16 +02:00
pendingCard
elevatedCard
2026-04-17 22:08:27 +02:00
}
2026-04-17 22:29:16 +02:00
postureCard
}
} else {
HStack ( spacing : 12 ) {
pendingCard
elevatedCard
postureCard
2026-04-17 22:08:27 +02:00
}
}
}
2026-04-17 22:29:16 +02:00
private var pendingCard : some View {
2026-04-17 22:08:27 +02:00
RequestSummaryMetricCard (
title : " Pending " ,
value : " \( pendingCount ) " ,
subtitle : pendingCount = = 0 ? " Queue is clear " : " Still waiting on your call " ,
accent : dashboardAccent
)
2026-04-17 22:29:16 +02:00
}
2026-04-17 22:08:27 +02:00
2026-04-17 22:29:16 +02:00
private var elevatedCard : some View {
2026-04-17 22:08:27 +02:00
RequestSummaryMetricCard (
title : " Elevated " ,
value : " \( elevatedCount ) " ,
subtitle : elevatedCount = = 0 ? " No privileged scopes " : " Needs slower review " ,
accent : . orange
)
2026-04-17 22:29:16 +02:00
}
2026-04-17 22:08:27 +02:00
2026-04-17 22:29:16 +02:00
private var postureCard : some View {
2026-04-17 22:08:27 +02:00
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 )
2026-04-17 22:29:16 +02:00
. background ( accent . opacity ( 0.10 ) , in : RoundedRectangle ( cornerRadius : 20 , style : . continuous ) )
. overlay (
RoundedRectangle ( cornerRadius : 20 , style : . continuous )
. stroke ( accent . opacity ( 0.08 ) , lineWidth : 1 )
)
2026-04-17 22:08:27 +02:00
}
}
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 )
2026-04-17 22:29:16 +02:00
. dashboardSurface ( radius : 24 )
2026-04-17 22:08:27 +02:00
}
@ 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 )
2026-04-17 22:29:16 +02:00
. 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 )
2026-04-17 22:08:27 +02:00
}
. 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 ) )
2026-04-17 22:29:16 +02:00
. overlay (
RoundedRectangle ( cornerRadius : 22 , style : . continuous )
. stroke ( requestAccent . opacity ( 0.08 ) , lineWidth : 1 )
)
2026-04-17 22:08:27 +02:00
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 : [
2026-04-17 22:29:16 +02:00
Color . white . opacity ( 0.92 ) ,
requestAccent . opacity ( 0.05 )
2026-04-17 22:08:27 +02:00
] ,
startPoint : . topLeading ,
endPoint : . bottomTrailing
) ,
in : RoundedRectangle ( cornerRadius : 28 , style : . continuous )
)
. overlay (
RoundedRectangle ( cornerRadius : 28 , style : . continuous )
2026-04-17 22:29:16 +02:00
. stroke ( requestAccent . opacity ( 0.10 ) , lineWidth : 1 )
2026-04-17 22:08:27 +02:00
)
2026-04-17 22:29:16 +02:00
. shadow ( color : dashboardShadow , radius : 12 , y : 5 )
2026-04-17 22:08:27 +02:00
}
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 )
2026-04-17 22:29:16 +02:00
. lineLimit ( 2 )
2026-04-17 22:08:27 +02:00
Text ( request . trustHeadline )
. font ( . subheadline . weight ( . semibold ) )
. foregroundStyle ( rowAccent )
2026-04-17 22:29:16 +02:00
. lineLimit ( 1 )
2026-04-17 22:08:27 +02:00
}
Spacer ( minLength : 0 )
StatusBadge (
title : request . status . title ,
tone : statusTone
)
}
Text ( request . source )
. font ( . subheadline )
. foregroundStyle ( . secondary )
2026-04-17 22:29:16 +02:00
. lineLimit ( 1 )
2026-04-17 22:08:27 +02:00
Text ( request . subtitle )
. font ( . footnote )
. foregroundStyle ( . secondary )
2026-04-17 22:29:16 +02:00
. lineLimit ( 1 )
2026-04-17 22:08:27 +02:00
. 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 )
2026-04-17 22:29:16 +02:00
. stroke ( isSelected ? rowAccent . opacity ( 0.36 ) : Color . clear , lineWidth : 1.5 )
2026-04-17 22:08:27 +02:00
)
2026-04-17 22:29:16 +02:00
. overlay ( alignment : . leading ) {
Capsule ( )
. fill ( rowAccent . opacity ( isSelected ? 0.80 : 0.30 ) )
. frame ( width : 5 )
. padding ( . vertical , 16 )
. padding ( . leading , 8 )
}
2026-04-17 22:08:27 +02:00
}
. buttonStyle ( . plain )
}
private var statusTone : Color {
switch request . status {
case . pending :
. orange
case . approved :
. green
case . rejected :
. red
}
}
private var backgroundStyle : Color {
2026-04-17 22:29:16 +02:00
isSelected ? rowAccent . opacity ( 0.08 ) : Color . white . opacity ( 0.90 )
2026-04-17 22:08:27 +02:00
}
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 ) {
2026-04-17 22:29:16 +02:00
ZStack ( alignment : . topLeading ) {
2026-04-17 22:08:27 +02:00
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 )
)
2026-04-17 22:29:16 +02:00
VStack ( alignment : . leading , spacing : 16 ) {
2026-04-17 22:08:27 +02:00
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 )
2026-04-17 22:29:16 +02:00
. font ( . title3 )
2026-04-17 22:08:27 +02:00
. 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 ) )
2026-04-17 22:29:16 +02:00
Text ( request . trustDetail )
. font ( . subheadline )
. foregroundStyle ( . white . opacity ( 0.82 ) )
2026-04-17 22:08:27 +02:00
}
. padding ( 24 )
}
2026-04-17 22:29:16 +02:00
. frame ( minHeight : 220 )
2026-04-17 22:08:27 +02:00
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 ) )
2026-04-17 22:29:16 +02:00
. overlay (
RoundedRectangle ( cornerRadius : 18 , style : . continuous )
. stroke ( accent . opacity ( 0.08 ) , lineWidth : 1 )
)
2026-04-17 22:08:27 +02:00
}
}
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 )
2026-04-17 22:29:16 +02:00
. dashboardSurface ( radius : 24 )
2026-04-17 22:08:27 +02:00
}
}
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 )
}
}
}
2026-04-17 22:29:16 +02:00
. padding ( . horizontal , DashboardSpacing . compactOuterPadding )
. padding ( . top , DashboardSpacing . compactTopPadding )
. padding ( . bottom , DashboardSpacing . compactBottomPadding )
. frame ( maxWidth : DashboardSpacing . compactContentWidth , alignment : . leading )
. frame ( maxWidth : . infinity , alignment : . leading )
2026-04-17 22:08:27 +02:00
}
. 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 )
2026-04-17 22:29:16 +02:00
. dashboardSurface ( radius : compactLayout ? 22 : 24 )
2026-04-17 22:08:27 +02:00
}
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 )
2026-04-17 22:29:16 +02:00
. background ( accent . opacity ( 0.10 ) , in : RoundedRectangle ( cornerRadius : 20 , style : . continuous ) )
. overlay (
RoundedRectangle ( cornerRadius : 20 , style : . continuous )
. stroke ( accent . opacity ( 0.08 ) , lineWidth : 1 )
)
2026-04-17 22:08:27 +02:00
}
}
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 ( )
}
2026-04-17 22:29:16 +02:00
. padding ( compactLayout ? DashboardSpacing . compactSectionPadding : DashboardSpacing . regularSectionPadding )
2026-04-17 22:08:27 +02:00
. frame ( maxWidth : . infinity , alignment : . leading )
2026-04-17 22:29:16 +02:00
. dashboardSurface ( radius : compactLayout ? DashboardSpacing . compactRadius : DashboardSpacing . regularRadius )
2026-04-17 22:08:27 +02:00
}
}
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 )
}
2026-04-17 22:29:16 +02:00
. padding ( . horizontal , 16 )
. padding ( . vertical , 12 )
. dashboardSurface ( radius : 999 , fillOpacity : 0.84 )
2026-04-17 22:08:27 +02:00
}
}
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 )
2026-04-17 22:29:16 +02:00
. dashboardSurface ( radius : 18 )
2026-04-17 22:08:27 +02:00
}
}
private struct StatusBadge : View {
let title : String
let tone : Color
var body : some View {
Text ( title )
. font ( . caption . weight ( . semibold ) )
2026-04-17 22:29:16 +02:00
. lineLimit ( 1 )
. minimumScaleFactor ( 0.8 )
. fixedSize ( horizontal : true , vertical : false )
2026-04-17 22:08:27 +02:00
. padding ( . horizontal , 10 )
. padding ( . vertical , 6 )
. background ( tone . opacity ( 0.14 ) , in : Capsule ( ) )
. foregroundStyle ( tone )
}
}
2026-04-17 22:29:16 +02:00
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 ( )
}
}