import SwiftUI let dashboardAccent = AppTheme.accent let dashboardGold = AppTheme.warmAccent extension View { @ViewBuilder func inlineNavigationTitleOnIOS() -> some View { #if os(iOS) navigationBarTitleDisplayMode(.inline) #else self #endif } @ViewBuilder func cleanTabBarOnIOS() -> some View { #if os(iOS) toolbarBackground(.visible, for: .tabBar) .toolbarBackground(AppTheme.chromeFill, for: .tabBar) #else self #endif } } struct HomeRootView: View { @ObservedObject var model: AppViewModel @State private var notificationBellFrame: CGRect? var body: some View { Group { if usesCompactNavigation { CompactHomeContainer(model: model) } else { RegularHomeContainer(model: model) } } .onPreferenceChange(NotificationBellFrameKey.self) { notificationBellFrame = $0 } .overlay(alignment: .topLeading) { if usesCompactNavigation { NotificationBellBadgeOverlay( unreadCount: model.unreadNotificationCount, bellFrame: notificationBellFrame ) .ignoresSafeArea() } } .sheet(isPresented: $model.isNotificationCenterPresented) { NotificationCenterSheet(model: model) } } private var usesCompactNavigation: Bool { #if os(iOS) true #else false #endif } } private struct CompactHomeContainer: View { @ObservedObject var model: AppViewModel @Environment(\.horizontalSizeClass) private var horizontalSizeClass var body: some View { TabView(selection: $model.selectedSection) { ForEach(AppSection.allCases) { section in NavigationStack { HomeSectionScreen(model: model, section: section, compactLayout: compactLayout) .navigationTitle(section.title) .inlineNavigationTitleOnIOS() .toolbar { DashboardToolbar(model: model) } } .tag(section) .tabItem { Label(section.title, systemImage: section.systemImage) } } } .cleanTabBarOnIOS() } private var compactLayout: Bool { #if os(iOS) horizontalSizeClass == .compact #else false #endif } } private struct RegularHomeContainer: View { @ObservedObject var model: AppViewModel var body: some View { NavigationSplitView { Sidebar(model: model) .navigationSplitViewColumnWidth(min: 240, ideal: 280, max: 320) } detail: { HomeSectionScreen(model: model, section: model.selectedSection, compactLayout: false) .navigationTitle(model.selectedSection.title) .toolbar { DashboardToolbar(model: model) } } .navigationSplitViewStyle(.balanced) } } private struct DashboardToolbar: ToolbarContent { @ObservedObject var model: AppViewModel var body: some ToolbarContent { ToolbarItemGroup(placement: .primaryAction) { NotificationBellButton(model: model) } } } struct NotificationBellFrameKey: PreferenceKey { static var defaultValue: CGRect? = nil static func reduce(value: inout CGRect?, nextValue: () -> CGRect?) { value = nextValue() ?? value } } private struct NotificationBellBadgeOverlay: View { let unreadCount: Int let bellFrame: CGRect? var body: some View { GeometryReader { proxy in if unreadCount > 0, let bellFrame { let rootFrame = proxy.frame(in: .global) Text("\(min(unreadCount, 9))") .font(.caption2.weight(.bold)) .foregroundStyle(.white) .frame(minWidth: 18, minHeight: 18) .padding(.horizontal, 3) .background(Color.orange, in: Capsule()) .position( x: bellFrame.maxX - rootFrame.minX - 2, y: bellFrame.minY - rootFrame.minY + 2 ) } } .allowsHitTesting(false) } } private struct HomeSectionScreen: View { @ObservedObject var model: AppViewModel let section: AppSection let compactLayout: Bool @State private var focusedRequest: ApprovalRequest? @State private var isOTPPresented = false @StateObject private var identifyReader = NFCIdentifyReader() var body: some View { AppScrollScreen( compactLayout: compactLayout, bottomPadding: compactLayout ? AppLayout.compactBottomDockPadding : AppLayout.regularBottomPadding ) { HomeTopActions( model: model, identifyReader: identifyReader, onScanQR: { model.isScannerPresented = true }, onShowOTP: { isOTPPresented = true } ) switch section { case .overview: OverviewPanel(model: model, compactLayout: compactLayout) case .requests: RequestsPanel(model: model, compactLayout: compactLayout, onOpenRequest: { focusedRequest = $0 }) case .activity: ActivityPanel(model: model, compactLayout: compactLayout) case .account: AccountPanel(model: model, compactLayout: compactLayout) } } .task { identifyReader.onAuthenticationRequestDetected = { request in Task { await model.identifyWithNFC(request) } } identifyReader.onError = { message in model.errorMessage = message } } .sheet(item: $focusedRequest) { request in RequestDetailSheet(request: request, model: model) } .sheet(isPresented: $model.isScannerPresented) { QRScannerSheet( seededPayload: model.session?.pairingCode ?? model.suggestedPairingPayload, title: "Scan proof QR", description: "Use the camera to scan an idp.global QR challenge from the site or device asking you to prove that it is really you.", navigationTitle: "Scan Proof QR", onCodeScanned: { payload in Task { await model.identifyWithPayload(payload, transport: .qr) } } ) } .sheet(isPresented: $isOTPPresented) { if let session = model.session { OneTimePasscodeSheet(session: session) } } } } private struct HomeTopActions: View { @ObservedObject var model: AppViewModel @ObservedObject var identifyReader: NFCIdentifyReader let onScanQR: () -> Void let onShowOTP: () -> Void var body: some View { LazyVGrid(columns: columns, spacing: 12) { identifyButton qrButton otpButton } } private var columns: [GridItem] { Array(repeating: GridItem(.flexible(), spacing: 12), count: 3) } private var identifyButton: some View { Button { identifyReader.beginScanning() } label: { AppActionTile( title: identifyReader.isScanning ? "Scanning NFC" : "Tap NFC", systemImage: "dot.radiowaves.left.and.right", tone: dashboardAccent, isBusy: identifyReader.isScanning || model.isIdentifying ) } .buttonStyle(.plain) .disabled(identifyReader.isScanning || !identifyReader.isSupported || model.isIdentifying) } private var qrButton: some View { Button { onScanQR() } label: { AppActionTile( title: "Scan QR", systemImage: "qrcode.viewfinder", tone: dashboardAccent ) } .buttonStyle(.plain) } private var otpButton: some View { Button { onShowOTP() } label: { AppActionTile( title: "OTP", systemImage: "number.square.fill", tone: dashboardGold ) } .buttonStyle(.plain) } } 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 Button { model.selectedSection = section } label: { HStack { Label(section.title, systemImage: section.systemImage) Spacer() if badgeCount(for: section) > 0 { AppStatusTag(title: "\(badgeCount(for: section))", tone: dashboardAccent) } } } .buttonStyle(.plain) .listRowBackground( model.selectedSection == section ? dashboardAccent.opacity(0.10) : Color.clear ) } } } .navigationTitle("idp.global") } private func badgeCount(for section: AppSection) -> Int { switch section { case .overview: 0 case .requests: model.pendingRequests.count case .activity: model.unreadNotificationCount case .account: 0 } } } private struct SidebarStatusCard: View { let profile: MemberProfile? let pendingCount: Int let unreadCount: Int var body: some View { VStack(alignment: .leading, spacing: 10) { Text("Digital Passport") .font(.headline) Text(profile?.handle ?? "No passport active") .foregroundStyle(.secondary) HStack(spacing: 8) { AppStatusTag(title: "\(pendingCount) pending", tone: dashboardAccent) AppStatusTag(title: "\(unreadCount) unread", tone: dashboardGold) } } .padding(.vertical, 6) } }