forked from sagnik/Project_Velocity
feat: Ipad app production readiness, Colony orchestration, Social posting
This commit is contained in:
@@ -1,21 +1,24 @@
|
||||
import SwiftUI
|
||||
import LocalAuthentication
|
||||
import UIKit
|
||||
|
||||
enum AppSection: String, CaseIterable, Hashable, Identifiable {
|
||||
var id: String { rawValue }
|
||||
case dashboard = "Dashboard"
|
||||
case clients = "Clients"
|
||||
case imports = "Imports"
|
||||
case communications = "Communications"
|
||||
case calendar = "Calendar"
|
||||
case oracle = "Oracle"
|
||||
case sentinel = "Sentinel"
|
||||
case inventory = "Inventory"
|
||||
case settings = "Settings"
|
||||
|
||||
var displayTitle: String {
|
||||
rawValue
|
||||
}
|
||||
|
||||
var dockTitle: String {
|
||||
switch self {
|
||||
case .sentinel:
|
||||
return SentinelScope.navigationTitle
|
||||
case .communications:
|
||||
return "Comms"
|
||||
default:
|
||||
return rawValue
|
||||
}
|
||||
@@ -25,11 +28,8 @@ enum AppSection: String, CaseIterable, Hashable, Identifiable {
|
||||
switch self {
|
||||
case .dashboard: return "square.grid.2x2"
|
||||
case .clients: return "person.text.rectangle"
|
||||
case .imports: return "tray.and.arrow.down"
|
||||
case .communications: return "phone.connection"
|
||||
case .calendar: return "calendar.badge.clock"
|
||||
case .oracle: return "message.and.waveform"
|
||||
case .sentinel: return "person.crop.rectangle"
|
||||
case .inventory: return "shippingbox"
|
||||
case .settings: return "gearshape"
|
||||
}
|
||||
@@ -39,11 +39,8 @@ enum AppSection: String, CaseIterable, Hashable, Identifiable {
|
||||
switch self {
|
||||
case .dashboard: return VelocityTheme.accent
|
||||
case .clients: return Color(red: 0.22, green: 0.78, blue: 0.96)
|
||||
case .imports: return Color(red: 0.94, green: 0.70, blue: 0.25)
|
||||
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 .oracle: return Color(red: 0.13, green: 0.83, blue: 0.93) // cyan
|
||||
case .sentinel: return Color(red: 0.60, green: 0.57, blue: 0.99) // indigo
|
||||
case .inventory: return VelocityTheme.warning
|
||||
case .settings: return VelocityTheme.mutedFg
|
||||
}
|
||||
@@ -51,102 +48,233 @@ enum AppSection: String, CaseIterable, Hashable, Identifiable {
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@State private var selectedSection: AppSection? = .dashboard
|
||||
@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 {
|
||||
NavigationSplitView(columnVisibility: .constant(.all)) {
|
||||
sidebarContent
|
||||
} detail: {
|
||||
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)
|
||||
}
|
||||
.navigationSplitViewStyle(.balanced)
|
||||
} else {
|
||||
ConfigurationGateView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Sidebar
|
||||
private var sidebarContent: some View {
|
||||
ZStack {
|
||||
VelocityTheme.sidebarBg.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// App title
|
||||
HStack(spacing: 10) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 9)
|
||||
.fill(VelocityTheme.accent.opacity(0.18))
|
||||
.frame(width: 34, height: 34)
|
||||
Image(systemName: "bolt.fill")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.foregroundStyle(VelocityTheme.accent)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text("Velocity")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
Text("Project Velocity · v1.1")
|
||||
.font(.system(size: 10))
|
||||
.foregroundStyle(VelocityTheme.mutedFg)
|
||||
}
|
||||
Spacer()
|
||||
.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"
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
Divider()
|
||||
.background(VelocityTheme.borderSubtle)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
// Nav items
|
||||
VStack(spacing: 2) {
|
||||
ForEach(AppSection.allCases) { section in
|
||||
Button {
|
||||
selectedSection = section
|
||||
} label: {
|
||||
SidebarRow(section: section, isSelected: selectedSection == section)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(section.displayTitle)
|
||||
.accessibilityAddTraits(selectedSection == section ? [.isSelected] : [])
|
||||
}
|
||||
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
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
Spacer()
|
||||
|
||||
// User footer
|
||||
Divider()
|
||||
.background(VelocityTheme.borderSubtle)
|
||||
HStack(spacing: 10) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(VelocityTheme.accent)
|
||||
.frame(width: 32, height: 32)
|
||||
Text(operatorInitials)
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(operatorName)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
Text(session.authModeDescription)
|
||||
.font(.system(size: 10))
|
||||
.foregroundStyle(VelocityTheme.mutedFg)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
.navigationTitle("")
|
||||
.toolbar(.hidden, for: .navigationBar)
|
||||
}
|
||||
|
||||
// MARK: – Detail
|
||||
@@ -156,16 +284,17 @@ struct ContentView: View {
|
||||
|
||||
Group {
|
||||
switch selectedSection {
|
||||
case .dashboard: DashboardView()
|
||||
case .dashboard:
|
||||
DashboardView { section in
|
||||
withAnimation(.interactiveSpring(response: 0.46, dampingFraction: 0.82)) {
|
||||
selectedSection = section
|
||||
}
|
||||
}
|
||||
case .clients: ClientsView()
|
||||
case .imports: ImportsView()
|
||||
case .communications: CommunicationsView()
|
||||
case .calendar: CalendarView()
|
||||
case .oracle: OracleView()
|
||||
case .sentinel: SentinelView()
|
||||
case .inventory: InventoryView()
|
||||
case .settings: SettingsView()
|
||||
case .none: DashboardView()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
@@ -185,37 +314,253 @@ struct ContentView: View {
|
||||
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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Sidebar Row
|
||||
private struct SidebarRow: View {
|
||||
let section: AppSection
|
||||
let isSelected: Bool
|
||||
private extension View {
|
||||
@ViewBuilder
|
||||
func `if`<Content: View>(_ 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 {
|
||||
HStack(spacing: 11) {
|
||||
Image(systemName: section.systemImage)
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundStyle(isSelected ? section.accentColor : VelocityTheme.mutedFg)
|
||||
.frame(width: 20)
|
||||
|
||||
Text(section.displayTitle)
|
||||
.font(.system(size: 14, weight: isSelected ? .semibold : .regular))
|
||||
.foregroundStyle(isSelected ? VelocityTheme.foreground : VelocityTheme.mutedFg)
|
||||
|
||||
Spacer()
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 9)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(isSelected ? section.accentColor.opacity(0.12) : .clear)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(isSelected ? section.accentColor.opacity(0.25) : .clear, lineWidth: 1)
|
||||
)
|
||||
.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
|
||||
}
|
||||
}
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user