# iOS Agent — UI Design Specs **Status**: DESIGN ONLY **Mirror**: Android Agent (`apps/android-agent/`) ## Overview This document defines the UI design for the iOS WireGuard agent. It mirrors the Android agent's design, colors, layout, and user experience. ## Color Palette ### Status Colors | Name | Hex | RGB | Usage | |------|-----|-----|-------| | `status_disconnected` | `#FFB0B0B0` | (176, 176, 176) | Disconnected state | | `status_connecting` | `#FFFFD93D` | (255, 217, 61) | Connecting state | | `status_connected` | `#FF4CAF50` | (76, 175, 80) | Connected state | | `status_failed` | `#FFFF6B6B` | (255, 107, 107) | Failed state | ### UI Colors | Name | Hex | RGB | Usage | |------|-----|-----|-------| | `card_bg` | `#FF1A1A2E` | (26, 26, 46) | Card background | | `card_bg_dark` | `#FF0F0F1A` | (15, 15, 26) | Badge background | | `text_primary` | `#FFFFFFFF` | (255, 255, 255) | Primary text | | `text_secondary` | `#FFB0B0B0` | (176, 176, 176) | Secondary text | | `accent_green` | `#FF4CAF50` | (76, 175, 80) | Accent/highlight | | `accent_blue` | `#FF2196F3` | (33, 150, 243) | Links/actions | | `border_color` | `#FF2A2A3E` | (42, 42, 62) | Card borders | ### Log Colors | Level | Hex | RGB | |-------|-----|-----| | `DEBUG` | `#FF6B6B6B` | (107, 107, 107) | | `INFO` | `#FFB0B0B0` | (176, 176, 176) | | `WARN` | `#FFFFD93D` | (255, 217, 61) | | `ERROR` | `#FFFF6B6B` | (255, 107, 107) | ## Color Definitions (Swift) ```swift import SwiftUI extension Color { // Status colors static let statusDisconnected = Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0 static let statusConnecting = Color(red: 1.0, green: 0.85, blue: 0.24) // #FFD93D static let statusConnected = Color(red: 0.30, green: 0.69, blue: 0.31) // #4CAF50 static let statusFailed = Color(red: 1.0, green: 0.42, blue: 0.42) // #FF6B6B // UI colors static let cardBackground = Color(red: 0.10, green: 0.10, blue: 0.18) // #1A1A2E static let cardBackgroundDark = Color(red: 0.06, green: 0.06, blue: 0.10) // #0F0F1A static let textPrimary = Color.white // #FFFFFF static let textSecondary = Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0 static let accentGreen = Color(red: 0.30, green: 0.69, blue: 0.31) // #4CAF50 static let accentBlue = Color(red: 0.13, green: 0.59, blue: 0.95) // #2196F3 static let borderColor = Color(red: 0.16, green: 0.16, blue: 0.24) // #2A2A3E } ``` ## Layout Structure ### Main Screen (ContentView) ``` ┌─────────────────────────────────────┐ │ NexusGuard [⚙️ Settings]│ ├─────────────────────────────────────┤ │ │ │ ┌─────────────────────────────┐ │ │ │ ● STATUS: Connected │ │ │ │ │ │ │ │ IP Address 10.172.21.2 │ │ │ │ Admin Web https://... │ │ │ │ Device ID abc-123-... │ │ │ │ Handshake 14:32:05 │ │ │ │ Allowed IPs 0.0.0.0/0 │ │ │ │ Transport HTTPS │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ [Status] [Ports] [Log] │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ Port Forwards: 2 │ │ │ │ TCP :8080 → 10.172.21.2:80│ │ │ │ UDP :53 → 10.172.21.2:53│ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ [Copy Log] [Clear Log] │ │ │ │ │ │ │ │ 14:32:05 [INFO] Agent... │ │ │ │ 14:32:05 [INFO] Tunnel... │ │ │ │ 14:32:05 [WARN] Retry... │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ ══════════════════ │ │ │ │ VPN Toggle │ │ │ └─────────────────────────────┘ │ │ │ └─────────────────────────────────────┘ ``` ### Settings Screen (SettingsView) ``` ┌─────────────────────────────────────┐ │ ← Settings │ ├─────────────────────────────────────┤ │ │ │ Server URL │ │ ┌─────────────────────────────┐ │ │ │ https://api-nexus.datadunia.com│ │ │ └─────────────────────────────┘ │ │ │ │ Registration Token │ │ ┌─────────────────────────────┐ │ │ │ •••••••••••••••• │ │ │ └─────────────────────────────┘ │ │ │ │ Auto-Start on Boot │ │ ┌─────────────────────────────┐ │ │ │ [Toggle] │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ Save Settings │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ Export Config │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ Import Config │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ Paste Config │ │ │ └─────────────────────────────┘ │ │ │ └─────────────────────────────────────┘ ``` ## Component Specs ### Status Card ```swift struct StatusCard: View { let state: AgentState let config: WireGuardConfig? var body: some View { VStack(alignment: .leading, spacing: 12) { // Status indicator HStack { Circle() .fill(statusColor) .frame(width: 12, height: 12) Text(statusText) .font(.headline) .foregroundColor(statusColor) } // Info fields if state == .connected { InfoRow(label: "IP Address", value: config?.internalIp ?? "-") InfoRow(label: "Admin Web", value: "https://api-nexus.datadunia.com") InfoRow(label: "Device ID", value: config?.deviceId ?? "-") InfoRow(label: "Handshake", value: lastHandshake ?? "-") InfoRow(label: "Allowed IPs", value: config?.allowedIps ?? "-") InfoRow(label: "Transport", value: transport) } } .padding() .background(Color.cardBackground) .cornerRadius(12) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(Color.borderColor, lineWidth: 1) ) } private var statusColor: Color { switch state { case .disconnected: return .statusDisconnected case .connecting: return .statusConnecting case .connected: return .statusConnected case .failed: return .statusFailed } } } ``` ### Info Row ```swift struct InfoRow: View { let label: String let value: String var body: some View { HStack { Text(label) .font(.subheadline) .foregroundColor(.textSecondary) Spacer() Text(value) .font(.subheadline) .foregroundColor(.textPrimary) .lineLimit(1) } } } ``` ### Port Forward Row ```swift struct PortForwardRow: View { let forward: PortForward var body: some View { HStack { Text(forward.protocol.uppercased()) .font(.caption) .fontWeight(.bold) .foregroundColor(.cardBackgroundDark) .padding(.horizontal, 8) .padding(.vertical, 4) .background(Color.accentGreen) .cornerRadius(4) Text(":\(forward.publicPort)") .font(.subheadline) .foregroundColor(.textPrimary) .font(.system(.body, design: .monospaced)) Spacer() Text("\(forward.targetIp):\(forward.targetPort)") .font(.subheadline) .foregroundColor(.textSecondary) .font(.system(.body, design: .monospaced)) } } } ``` ### Log Entry ```swift struct LogEntryView: View { let entry: LogEntry var body: some View { HStack(alignment: .top) { Text(entry.timestamp, style: .time) .font(.system(.caption, design: .monospaced)) .foregroundColor(entry.level.color) Text("[\(entry.level.rawValue)]") .font(.system(.caption, design: .monospaced)) .foregroundColor(entry.level.color) Text("\(entry.tag): \(entry.message)") .font(.system(.caption, design: .monospaced)) .foregroundColor(entry.level.color) } } } extension LogLevel { var color: Color { switch self { case .debug: return Color(red: 0.42, green: 0.42, blue: 0.42) // #6B6B6B case .info: return Color(red: 0.69, green: 0.69, blue: 0.69) // #B0B0B0 case .warn: return Color(red: 1.0, green: 0.85, blue: 0.24) // #FFD93D case .error: return Color(red: 1.0, green: 0.42, blue: 0.42) // #FF6B6B } } } ``` ### VPN Toggle ```swift struct VPNToggle: View { @Binding var isOn: Bool let action: () -> Void var body: some View { Button(action: action) { RoundedRectangle(cornerRadius: 25) .fill(isOn ? Color.accentGreen : Color.statusDisconnected) .frame(height: 50) .overlay( HStack { Circle() .fill(Color.white) .frame(width: 40, height: 40) .offset(x: isOn ? 40 : -40) Spacer() } ) } .buttonStyle(PlainButtonStyle()) } } ``` ## Tab Layout ```swift struct TabBar: View { @Binding var selectedTab: Int var body: some View { HStack(spacing: 0) { TabButton(title: "Status", isSelected: selectedTab == 0) { selectedTab = 0 } TabButton(title: "Ports", isSelected: selectedTab == 1) { selectedTab = 1 } TabButton(title: "Log", isSelected: selectedTab == 2) { selectedTab = 2 } } .background(Color.cardBackground) .cornerRadius(8) } } struct TabButton: View { let title: String let isSelected: Bool let action: () -> Void var body: some View { Button(action: action) { Text(title) .font(.subheadline) .foregroundColor(isSelected ? .textPrimary : .textSecondary) .padding(.vertical, 12) .frame(maxWidth: .infinity) .background(isSelected ? Color.accentGreen : Color.clear) .cornerRadius(8) } } } ``` ## Typography ### Font Sizes | Element | Size | Weight | Design | |---------|------|--------|--------| | Status text | 18pt | Semibold | Default | | Info label | 14pt | Regular | Default | | Info value | 14pt | Regular | Monospaced | | Tab text | 14pt | Medium | Default | | Log text | 12pt | Regular | Monospaced | | Badge text | 10pt | Bold | Default | ### Font Definitions ```swift extension Font { static let statusText = Font.headline static let infoLabel = Font.subheadline static let infoValue = Font.subheadline.monospaced() static let tabText = Font.subheadline.weight(.medium) static let logText = Font.caption.monospaced() static let badgeText = Font.caption2.bold() } ``` ## Spacing | Element | Value | |---------|-------| | Card padding | 16pt | | Card corner radius | 12pt | | Card border width | 1pt | | Row spacing | 12pt | | Badge padding | 8pt horizontal, 4pt vertical | | Badge corner radius | 4pt | | Button height | 50pt | | Button corner radius | 25pt | ## Animation ### Status Change ```swift withAnimation(.easeInOut(duration: 0.3)) { state = .connected } ``` ### Tab Switch ```swift withAnimation(.easeInOut(duration: 0.2)) { selectedTab = 1 } ``` ### VPN Toggle ```swift withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) { isVPNOn.toggle() } ``` ## Haptic Feedback ```swift // Success UIImpactFeedbackGenerator(style: .medium).impactOccurred() // Error UINotificationFeedbackGenerator().notificationOccurred(.error) // Selection UISelectionFeedbackGenerator().selectionChanged() ``` ## Dark Mode iOS agent uses dark mode only (matches Android design). Force dark mode: ```swift @main struct NexusGuardApp: App { var body: some Scene { WindowGroup { ContentView() .preferredColorScheme(.dark) } } } ``` ## Responsive Design ### iPhone SE - Compact layout - Smaller font sizes - Reduced padding ### iPhone 14/15 - Standard layout - Default font sizes - Standard padding ### iPhone 14/15 Pro Max - Expanded layout - Larger font sizes - Increased padding ## Accessibility ### VoiceOver ```swift Text("Connected") .accessibilityLabel("VPN Status: Connected") .accessibilityHint("Double tap to toggle VPN") InfoRow(label: "IP Address", value: "10.172.21.2") .accessibilityElement(children: .combine) .accessibilityLabel("IP Address: 10.172.21.2") ``` ### Dynamic Type ```swift Text("Status") .font(.headline) .dynamicTypeSize(...DynamicTypeSize.accessibility2) ``` ### Reduce Motion ```swift @Environment(\.accessibilityReduceMotion) var reduceMotion withAnimation(reduceMotion ? nil : .easeInOut(duration: 0.3)) { state = .connected } ```