Files
Nexus-Guard-Suite/reference/ios-agent/docs/UI_DESIGN.md
T
datadunia 84689b208e docs: add ESP32 and iOS agent architecture reference
- reference/esp32-agent/: DESIGN, HARDWARE, API_COMPAT, ARCHITECTURE, README

- reference/ios-agent/: DESIGN, API_COMPAT, UI_DESIGN, ARCHITECTURE, README

- Both mirror Android agent architecture, UI design, and heartbeat flow

- Architecture-only (no code) — avoids submodule conflicts

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-07-06 14:20:10 +07:00

16 KiB

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)

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

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

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

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

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

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

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

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

withAnimation(.easeInOut(duration: 0.3)) {
    state = .connected
}

Tab Switch

withAnimation(.easeInOut(duration: 0.2)) {
    selectedTab = 1
}

VPN Toggle

withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
    isVPNOn.toggle()
}

Haptic Feedback

// 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:

@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

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

Text("Status")
    .font(.headline)
    .dynamicTypeSize(...DynamicTypeSize.accessibility2)

Reduce Motion

@Environment(\.accessibilityReduceMotion) var reduceMotion

withAnimation(reduceMotion ? nil : .easeInOut(duration: 0.3)) {
    state = .connected
}