import SwiftUI struct LoginView: View { @EnvironmentObject var store: ConnectionStore @StateObject private var vm = LoginViewModel() @State private var appeared = false var body: some View { ZStack { AppTheme.background.ignoresSafeArea() VStack(spacing: 0) { Spacer() // Logo + wordmark VStack(spacing: 20) { Image("nas_logo_clean") .resizable() .scaledToFit() .frame(width: 80, height: 80) .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) .shadow(color: .black.opacity(0.12), radius: 20, x: 0, y: 6) .scaleEffect(appeared ? 1 : 0.85) .opacity(appeared ? 1 : 0) .animation(.spring(response: 0.5, dampingFraction: 0.72).delay(0.08), value: appeared) VStack(spacing: 6) { Text("Kisani") .font(.system(size: 32, weight: .bold, design: .default)) .foregroundStyle(AppTheme.ink) .opacity(appeared ? 1 : 0) .animation(.spring(response: 0.45, dampingFraction: 0.8).delay(0.16), value: appeared) Text("Your NAS, always in sync.") .font(.system(size: 15, weight: .regular)) .foregroundStyle(AppTheme.inkTertiary) .opacity(appeared ? 1 : 0) .animation(.spring(response: 0.45, dampingFraction: 0.8).delay(0.22), value: appeared) } } Spacer().frame(height: 52) // Auth button + error VStack(spacing: 12) { Button(action: { vm.authenticateWithFaceID() }) { HStack(spacing: 8) { if vm.isAuthenticating { ProgressView() .tint(AppTheme.inkInverse) .scaleEffect(0.85) } else { Image(systemName: "faceid") .font(.system(size: 17, weight: .medium)) Text("Sign in with Face ID") .font(.system(size: 15, weight: .semibold)) } } } .buttonStyle(PrimaryButtonStyle()) .padding(.horizontal, AppTheme.hPad) .disabled(vm.isAuthenticating) .opacity(appeared ? 1 : 0) .animation(.spring(response: 0.45, dampingFraction: 0.8).delay(0.28), value: appeared) if let error = vm.authError { HStack(spacing: 5) { Image(systemName: "exclamationmark.circle") .font(.system(size: 12)) Text(error) .font(AppTheme.caption()) } .foregroundStyle(AppTheme.destructive) .transition(.opacity.combined(with: .move(edge: .top))) } } .animation(.spring(response: 0.3, dampingFraction: 0.8), value: vm.authError != nil) Spacer() // More options Button("More options") { vm.showMoreSheet = true } .font(.system(size: 13, weight: .regular)) .foregroundStyle(AppTheme.inkTertiary) .padding(.bottom, 48) .opacity(appeared ? 1 : 0) .animation(.spring(response: 0.45, dampingFraction: 0.8).delay(0.36), value: appeared) } } .sheet(isPresented: $vm.showMoreSheet) { MoreOptionsSheet(isPresented: $vm.showMoreSheet) { vm.showMoreSheet = false store.isSessionActive = true } .presentationDetents([.height(200)]) .modifier(SheetCornerRadius(20)) } .onAppear { vm.onAuthenticated = { store.isSessionActive = true } appeared = true } } } struct MoreOptionsSheet: View { @Binding var isPresented: Bool var onAuthenticated: (() -> Void)? var body: some View { VStack(spacing: 0) { Capsule() .fill(AppTheme.inkQuaternary) .frame(width: 32, height: 4) .padding(.top, 12) .padding(.bottom, 20) VStack(spacing: 0) { sheetRow("Password", weight: .medium) { } Divider().padding(.leading, 16) sheetRow("Another account") { isPresented = false onAuthenticated?() } Divider().padding(.leading, 16) sheetRow("Cancel", color: AppTheme.inkTertiary) { isPresented = false } } .background(AppTheme.surfaceRaised) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous) .stroke(AppTheme.ink.opacity(0.08), lineWidth: 0.5) ) .padding(.horizontal, AppTheme.hPad) .padding(.bottom, 16) } .background(AppTheme.background) } private func sheetRow( _ title: String, weight: Font.Weight = .regular, color: Color = AppTheme.ink, action: @escaping () -> Void ) -> some View { Button(action: action) { Text(title) .font(.system(size: 15, weight: weight)) .foregroundStyle(color) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 16) .padding(.vertical, 14) } .buttonStyle(ScaleButtonStyle()) } } private struct SheetCornerRadius: ViewModifier { let radius: CGFloat init(_ radius: CGFloat) { self.radius = radius } func body(content: Content) -> some View { if #available(iOS 16.4, *) { content.presentationCornerRadius(radius) } else { content } } }