import SwiftUI import LocalAuthentication import UIKit enum AppSection: String, CaseIterable, Hashable, Identifiable { var id: String { rawValue } case dashboard = "Dashboard" case clients = "Clients" case communications = "Communications" case calendar = "Calendar" case inventory = "Inventory" case settings = "Settings" var displayTitle: String { rawValue } var dockTitle: String { switch self { case .communications: return "Comms" default: return rawValue } } var systemImage: String { switch self { case .dashboard: return "square.grid.2x2" case .clients: return "person.text.rectangle" case .communications: return "phone.connection" case .calendar: return "calendar.badge.clock" case .inventory: return "shippingbox" case .settings: return "gearshape" } } var accentColor: Color { switch self { case .dashboard: return VelocityTheme.accent case .clients: return Color(red: 0.22, green: 0.78, blue: 0.96) case .communications: return Color(red: 0.19, green: 0.84, blue: 0.63) case .calendar: return Color(red: 0.96, green: 0.67, blue: 0.16) case .inventory: return VelocityTheme.warning case .settings: return VelocityTheme.mutedFg } } } struct ContentView: View { @State private var selectedSection: AppSection = .dashboard @State private var session = SessionStore.shared @State private var store = AppStore.shared @State private var isOraclePresented = false @State private var dockFocusedSection: AppSection? @State private var isPrivacyLocked = true @State private var isAuthenticating = false @State private var privacyMessage = "Unlock Velocity" @Environment(\.scenePhase) private var scenePhase @Namespace private var dockSelectionNamespace var body: some View { Group { if session.isConfigured { ZStack(alignment: .bottom) { detailContent floatingNavigationPill .padding(.horizontal, 24) .padding(.bottom, 24) } .overlay(alignment: .bottomTrailing) { OracleFloatingOrb(alertCount: oracleAlertCount) { isOraclePresented = true } .padding(.trailing, 28) .padding(.bottom, selectedSection == .clients ? 154 : 112) } .overlay(alignment: .top) { OfflineSyncGlow(isActive: hasPendingSync) } .overlay(alignment: .top) { VaultShareToast( message: store.vaultShareMessage, error: store.vaultShareError ) { withAnimation(.interactiveSpring(response: 0.35, dampingFraction: 0.86)) { store.vaultShareMessage = nil store.vaultShareError = nil } } .padding(.top, 18) } .overlay { if store.isShowroomModeEnabled, store.errorMessage != nil { ShowroomAmbientFallback() .transition(.opacity.animation(.interactiveSpring(response: 0.55, dampingFraction: 0.9))) } } .overlay { if isPrivacyLocked { PrivacyLockOverlay( message: privacyMessage, isAuthenticating: isAuthenticating ) { authenticateSession() } .transition(.opacity.animation(.interactiveSpring(response: 0.35, dampingFraction: 0.88))) } } .sheet(isPresented: $isOraclePresented) { OracleConciergeSheet() .presentationDetents([.medium, .large]) .presentationDragIndicator(.visible) } } else { ConfigurationGateView() } } .onAppear { authenticateSession() } .onChange(of: scenePhase) { _, phase in switch phase { case .background, .inactive: withAnimation(.interactiveSpring(response: 0.35, dampingFraction: 0.9)) { isPrivacyLocked = true privacyMessage = "Velocity is locked" } case .active: if isPrivacyLocked { authenticateSession() } @unknown default: break } } } private var floatingNavigationPill: some View { VStack(spacing: 6) { HStack(alignment: .bottom, spacing: 12) { ForEach(AppSection.allCases) { section in let isSelected = selectedSection == section let isFocused = dockFocusedSection == section Button { UIImpactFeedbackGenerator(style: .soft).impactOccurred() withAnimation(.interactiveSpring(response: 0.46, dampingFraction: 0.78)) { selectedSection = section dockFocusedSection = section } hideDockTooltipAfterTap(for: section) } label: { dockItem(for: section, isSelected: isSelected, isFocused: isFocused) } .buttonStyle(.plain) .accessibilityLabel(section.dockTitle) .accessibilityAddTraits(isSelected ? [.isSelected] : []) .onHover { hovering in withAnimation(.interactiveSpring(response: 0.34, dampingFraction: 0.76)) { dockFocusedSection = hovering ? section : nil } } } } .padding(.horizontal, 13) .padding(.top, 9) .padding(.bottom, 8) } .background( RoundedRectangle(cornerRadius: 30, style: .continuous) .fill(.ultraThinMaterial) .background( RoundedRectangle(cornerRadius: 30, style: .continuous) .fill(Color.black.opacity(0.34)) .blur(radius: 0.5) ) .shadow(color: Color.black.opacity(0.42), radius: 28, y: 18) ) .overlay( RoundedRectangle(cornerRadius: 30, style: .continuous) .stroke(Color.white.opacity(0.16), lineWidth: 1) ) .frame(maxWidth: .infinity, alignment: .center) } @ViewBuilder private func dockItem(for section: AppSection, isSelected: Bool, isFocused: Bool) -> some View { VStack(spacing: 7) { ZStack { RoundedRectangle(cornerRadius: 16, style: .continuous) .fill( LinearGradient( colors: [ section.accentColor.opacity(isSelected ? 0.34 : 0.18), Color.white.opacity(isSelected ? 0.12 : 0.05), ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .overlay( RoundedRectangle(cornerRadius: 16, style: .continuous) .stroke(Color.white.opacity(isSelected ? 0.26 : 0.12), lineWidth: 1) ) .shadow( color: section.accentColor.opacity(isSelected || isFocused ? 0.32 : 0.10), radius: isSelected || isFocused ? 16 : 7, y: isSelected || isFocused ? 8 : 3 ) .if(isSelected) { view in view.matchedGeometryEffect(id: "selectedDockTile", in: dockSelectionNamespace) } Image(systemName: section.systemImage) .font(.system(size: isFocused ? 24 : (isSelected ? 22 : 19), weight: .semibold)) .foregroundStyle(isSelected ? VelocityTheme.foreground : section.accentColor) } .frame(width: 50, height: 50) .scaleEffect(isFocused ? 1.34 : (isSelected ? 1.16 : 1.0), anchor: .bottom) .offset(y: isFocused ? -13 : (isSelected ? -5 : 0)) Circle() .fill(isSelected ? section.accentColor.opacity(0.92) : Color.clear) .frame(width: 5, height: 5) .shadow(color: section.accentColor.opacity(isSelected ? 0.45 : 0), radius: 5) } .frame(width: 58, height: 70, alignment: .bottom) .overlay(alignment: .top) { if isFocused { dockTooltip(section.dockTitle) .offset(y: -46) .transition( .asymmetric( insertion: .scale(scale: 0.88, anchor: .bottom).combined(with: .opacity), removal: .opacity ) ) } } .contentShape(Rectangle()) .animation(.interactiveSpring(response: 0.44, dampingFraction: 0.76), value: isSelected) .animation(.interactiveSpring(response: 0.32, dampingFraction: 0.70), value: isFocused) } private func dockTooltip(_ title: String) -> some View { VStack(spacing: 0) { Text(title) .font(.system(size: 12, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) .lineLimit(1) .padding(.horizontal, 11) .padding(.vertical, 6) .background( RoundedRectangle(cornerRadius: 6, style: .continuous) .fill(Color(red: 0.13, green: 0.13, blue: 0.13).opacity(0.96)) .overlay( RoundedRectangle(cornerRadius: 6, style: .continuous) .stroke(Color.white.opacity(0.18), lineWidth: 1) ) ) Triangle() .fill(Color(red: 0.13, green: 0.13, blue: 0.13).opacity(0.96)) .frame(width: 12, height: 7) } .fixedSize() .shadow(color: Color.black.opacity(0.42), radius: 10, y: 6) } private func hideDockTooltipAfterTap(for section: AppSection) { Task { try? await Task.sleep(nanoseconds: 1_200_000_000) await MainActor.run { guard dockFocusedSection == section else { return } withAnimation(.interactiveSpring(response: 0.32, dampingFraction: 0.78)) { dockFocusedSection = nil } } } } // MARK: – Detail private var detailContent: some View { ZStack { VelocityTheme.background.ignoresSafeArea() Group { switch selectedSection { case .dashboard: DashboardView { section in withAnimation(.interactiveSpring(response: 0.46, dampingFraction: 0.82)) { selectedSection = section } } case .clients: ClientsView() case .communications: CommunicationsView() case .calendar: CalendarView() case .inventory: InventoryView() case .settings: SettingsView() } } .frame(maxWidth: .infinity, maxHeight: .infinity) } } private var operatorName: String { session.operatorIdentity } private var operatorInitials: String { let source = session.operatorIdentity let parts = source .replacingOccurrences(of: "@", with: " ") .split(separator: ".") .flatMap { $0.split(separator: " ") } let initials = parts.prefix(2).compactMap(\.first) return initials.isEmpty ? "VO" : String(initials) } private var hasPendingSync: Bool { !store.isShowroomModeEnabled && (!store.pendingSyncTaskIDs.isEmpty || !store.pendingSyncCalendarEventIDs.isEmpty) } private var oracleAlertCount: Int { guard let alertSnapshot = store.alertSnapshot else { return 0 } return alertSnapshot.pendingInsights + alertSnapshot.pendingTranscriptions + alertSnapshot.upcomingCalendarEvents24h } private func authenticateSession() { guard session.isConfigured, !isAuthenticating else { return } isAuthenticating = true privacyMessage = "Authenticating..." let context = LAContext() context.localizedCancelTitle = "Lock" var error: NSError? let policy: LAPolicy = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) ? .deviceOwnerAuthenticationWithBiometrics : .deviceOwnerAuthentication guard context.canEvaluatePolicy(policy, error: &error) else { isAuthenticating = false privacyMessage = error?.localizedDescription ?? "Device authentication is unavailable." return } context.evaluatePolicy(policy, localizedReason: "Unlock Project Velocity") { success, authError in Task { @MainActor in isAuthenticating = false withAnimation(.interactiveSpring(response: 0.38, dampingFraction: 0.86)) { isPrivacyLocked = !success } privacyMessage = success ? "Unlocked" : (authError?.localizedDescription ?? "Authentication failed.") } } } } private extension View { @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { if condition { transform(self) } else { self } } } private struct Triangle: Shape { func path(in rect: CGRect) -> Path { var path = Path() path.move(to: CGPoint(x: rect.midX, y: rect.maxY)) path.addLine(to: CGPoint(x: rect.minX, y: rect.minY)) path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) path.closeSubpath() return path } } private struct OracleFloatingOrb: View { let alertCount: Int let action: () -> Void @State private var isPressed = false var body: some View { Button { UIImpactFeedbackGenerator(style: .soft).impactOccurred() action() } label: { ZStack { Circle() .fill(.ultraThinMaterial) .frame(width: 64, height: 64) .overlay( Circle() .stroke(Color(red: 0.13, green: 0.83, blue: 0.93).opacity(0.55), lineWidth: 1) ) .shadow(color: Color(red: 0.13, green: 0.83, blue: 0.93).opacity(0.35), radius: isPressed ? 10 : 18) Image(systemName: "sparkles") .font(.system(size: 24, weight: .semibold)) .foregroundStyle(Color(red: 0.68, green: 0.95, blue: 1.0)) if alertCount > 0 { Text(alertCount > 99 ? "99+" : "\(alertCount)") .font(.system(size: 9, weight: .bold)) .foregroundStyle(.white) .padding(.horizontal, 6) .padding(.vertical, 3) .background(Capsule().fill(VelocityTheme.danger)) .offset(x: 22, y: -22) .transition(.scale.combined(with: .opacity)) } } } .buttonStyle(.plain) .accessibilityLabel("Open Oracle") .simultaneousGesture( DragGesture(minimumDistance: 0) .onChanged { _ in withAnimation(.interactiveSpring(response: 0.24, dampingFraction: 0.8)) { isPressed = true } } .onEnded { _ in withAnimation(.interactiveSpring(response: 0.28, dampingFraction: 0.82)) { isPressed = false } } ) } } private struct OfflineSyncGlow: View { let isActive: Bool var body: some View { Rectangle() .fill(VelocityTheme.warning.opacity(isActive ? 0.28 : 0)) .frame(height: isActive ? 5 : 0) .shadow(color: VelocityTheme.warning.opacity(isActive ? 0.65 : 0), radius: 14, y: 5) .animation(.interactiveSpring(response: 0.42, dampingFraction: 0.9), value: isActive) .ignoresSafeArea(edges: .top) } } private struct VaultShareToast: View { let message: String? let error: String? let dismiss: () -> Void var body: some View { if let text = message ?? error { HStack(spacing: 10) { Image(systemName: error == nil ? "link.circle.fill" : "exclamationmark.triangle.fill") .foregroundStyle(error == nil ? VelocityTheme.success : VelocityTheme.warning) Text(text) .font(.system(size: 12, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Button { dismiss() } label: { Image(systemName: "xmark") .font(.system(size: 10, weight: .bold)) .foregroundStyle(VelocityTheme.mutedFg) } .buttonStyle(.plain) } .padding(.horizontal, 14) .padding(.vertical, 9) .background( Capsule() .fill(.ultraThinMaterial) .overlay(Capsule().stroke(Color.white.opacity(0.16), lineWidth: 1)) ) .transition(.move(edge: .top).combined(with: .opacity)) } } } private struct ShowroomAmbientFallback: View { var body: some View { ZStack { LinearGradient( colors: [ Color.black.opacity(0.88), Color(red: 0.045, green: 0.055, blue: 0.075).opacity(0.96), Color.black.opacity(0.92), ], startPoint: .topLeading, endPoint: .bottomTrailing ) .ignoresSafeArea() VStack(spacing: 14) { Image(systemName: "building.2.crop.circle") .font(.system(size: 58, weight: .light)) .foregroundStyle(.white.opacity(0.72)) Text("Velocity") .font(.system(size: 34, weight: .semibold)) .foregroundStyle(.white) Text("Preparing the showroom view") .font(.system(size: 13, weight: .medium)) .foregroundStyle(.white.opacity(0.58)) } } } } private struct PrivacyLockOverlay: View { let message: String let isAuthenticating: Bool let unlock: () -> Void var body: some View { ZStack { Rectangle() .fill(.ultraThinMaterial) .ignoresSafeArea() Color.black.opacity(0.62) .ignoresSafeArea() VStack(spacing: 16) { Image(systemName: "faceid") .font(.system(size: 50, weight: .light)) .foregroundStyle(VelocityTheme.foreground) Text("Velocity") .font(.system(size: 30, weight: .semibold, design: .default)) .foregroundStyle(VelocityTheme.foreground) Text(message) .font(.system(size: 13, weight: .medium)) .foregroundStyle(VelocityTheme.mutedFg) Button { unlock() } label: { HStack(spacing: 8) { if isAuthenticating { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) } else { Image(systemName: "lock.open") } Text(isAuthenticating ? "Unlocking" : "Unlock") } .font(.system(size: 14, weight: .semibold)) .foregroundStyle(.white) .padding(.horizontal, 20) .padding(.vertical, 11) .background(Capsule().fill(VelocityTheme.accent)) } .buttonStyle(.plain) .disabled(isAuthenticating) } .padding(28) .background( RoundedRectangle(cornerRadius: 26) .fill(Color.black.opacity(0.32)) .overlay( RoundedRectangle(cornerRadius: 26) .stroke(Color.white.opacity(0.16), lineWidth: 1) ) ) } } } #Preview { ContentView() }