From b5cf3d9e01acacf8b98e3720abec630ac7bfbf2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Kunz?= Date: Sat, 18 Apr 2026 06:11:07 +0200 Subject: [PATCH] Polish watch companion layouts --- WatchApp/Features/WatchRootView.swift | 284 ++++++++++++++++++-------- 1 file changed, 197 insertions(+), 87 deletions(-) diff --git a/WatchApp/Features/WatchRootView.swift b/WatchApp/Features/WatchRootView.swift index 0f43c8c..55a69bd 100644 --- a/WatchApp/Features/WatchRootView.swift +++ b/WatchApp/Features/WatchRootView.swift @@ -28,25 +28,34 @@ private struct WatchPairingView: View { var body: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { - AppPanel(compactLayout: true, radius: 22) { + VStack(alignment: .leading, spacing: 10) { AppBadge(title: "Preview passport", tone: watchAccent) Text("Prove identity from your wrist") .font(.title3.weight(.semibold)) + .foregroundStyle(.white) - Text("This preview connects directly to the mock service today.") + Text("Link this watch to the preview passport so identity checks and alerts stay visible on your wrist.") .font(.footnote) - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.72)) HStack(spacing: 8) { AppStatusTag(title: "Wrist-ready", tone: watchAccent) - AppStatusTag(title: "Preview sync", tone: watchGold) + AppStatusTag(title: "Proof focus", tone: watchGold) } } + .watchCard() if model.isBootstrapping { - ProgressView("Preparing preview passport...") - .frame(maxWidth: .infinity, alignment: .leading) + HStack(spacing: 8) { + ProgressView() + .tint(watchAccent) + Text("Preparing preview passport...") + .font(.footnote) + .foregroundStyle(.white.opacity(0.72)) + } + .frame(maxWidth: .infinity, alignment: .leading) + .watchCard() } Button { @@ -58,47 +67,82 @@ private struct WatchPairingView: View { ProgressView() .frame(maxWidth: .infinity) } else { - Label("Use Preview Passport", systemImage: "qrcode") + Label("Link Preview Passport", systemImage: "applewatch") .frame(maxWidth: .infinity) } } .buttonStyle(.borderedProminent) + .tint(watchAccent) .disabled(model.isBootstrapping || model.suggestedPairingPayload.isEmpty || model.isAuthenticating) - AppPanel(compactLayout: true, radius: 18) { - Text("What works today") + VStack(alignment: .leading, spacing: 10) { + Text("What this watch does") .font(.headline) + .foregroundStyle(.white) - Text("The watch shows pending identity checks, recent alerts, and quick actions.") - .font(.footnote) - .foregroundStyle(.secondary) + WatchSetupFeatureRow( + systemImage: "checkmark.shield", + title: "Review identity checks", + subtitle: "See pending proof prompts quickly." + ) + + WatchSetupFeatureRow( + systemImage: "bell.badge", + title: "Surface important alerts", + subtitle: "Keep passport activity visible at a glance." + ) + + WatchSetupFeatureRow( + systemImage: "iphone.radiowaves.left.and.right", + title: "Stay in sync with the phone preview", + subtitle: "Use the same mocked passport context." + ) } + .watchCard() } .padding(.horizontal, 8) + .padding(.top, 6) .padding(.bottom, 20) } - .navigationTitle("Set Up Watch") + .background(Color.black.ignoresSafeArea()) + .navigationTitle("Link Watch") } } -private struct WatchInfoPill: View { +private struct WatchSetupFeatureRow: View { + let systemImage: String let title: String - let value: String - let tone: Color + let subtitle: String var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundStyle(.secondary) - Text(value) - .font(.caption.weight(.semibold)) - .foregroundStyle(.primary) + HStack(alignment: .top, spacing: 10) { + Image(systemName: systemImage) + .font(.footnote.weight(.semibold)) + .foregroundStyle(watchAccent) + .frame(width: 18, height: 18) + + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.footnote.weight(.semibold)) + .foregroundStyle(.white) + + Text(subtitle) + .font(.caption2) + .foregroundStyle(.white.opacity(0.68)) + } } - .padding(.horizontal, 10) - .padding(.vertical, 8) - .frame(maxWidth: .infinity, alignment: .leading) - .background(tone.opacity(0.10), in: RoundedRectangle(cornerRadius: 16, style: .continuous)) + } +} + +private extension View { + func watchCard() -> some View { + padding(14) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.white.opacity(0.08), in: RoundedRectangle(cornerRadius: 22, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 22, style: .continuous) + .stroke(Color.white.opacity(0.10), lineWidth: 1) + ) } } @@ -106,94 +150,131 @@ private struct WatchDashboardView: View { @ObservedObject var model: AppViewModel var body: some View { - List { - Section { + ScrollView { + VStack(alignment: .leading, spacing: 12) { WatchPassportCard(model: model) - } + .watchCard() + + WatchSectionHeader( + title: "Pending", + detail: model.pendingRequests.isEmpty ? nil : "\(model.pendingRequests.count)" + ) - Section("Pending") { if model.pendingRequests.isEmpty { - Text("No checks waiting.") - .foregroundStyle(.secondary) + VStack(alignment: .leading, spacing: 10) { + Text("No checks waiting.") + .font(.footnote.weight(.semibold)) + .foregroundStyle(.white) - Button("Seed Identity Check") { - Task { - await model.simulateIncomingRequest() + Text("New identity checks will appear here when a site or device asks you to prove it is really you.") + .font(.caption2) + .foregroundStyle(.white.opacity(0.68)) + + Button("Seed Identity Check") { + Task { + await model.simulateIncomingRequest() + } } + .buttonStyle(.bordered) + .tint(watchAccent) } + .watchCard() } else { ForEach(model.pendingRequests) { request in NavigationLink { WatchRequestDetailView(model: model, requestID: request.id) } label: { WatchRequestRow(request: request) + .watchCard() } + .buttonStyle(.plain) } } - } - Section("Recent Activity") { + WatchSectionHeader(title: "Activity") + if model.notifications.isEmpty { - Text("No recent alerts.") - .foregroundStyle(.secondary) + VStack(alignment: .leading, spacing: 8) { + Text("No recent alerts.") + .font(.footnote.weight(.semibold)) + .foregroundStyle(.white) + + Text("Passport activity and security events will show up here.") + .font(.caption2) + .foregroundStyle(.white.opacity(0.68)) + } + .watchCard() } else { ForEach(model.notifications.prefix(3)) { notification in NavigationLink { WatchNotificationDetailView(model: model, notificationID: notification.id) } label: { WatchNotificationRow(notification: notification) + .watchCard() } - } - } - } - - Section("Actions") { - Button("Refresh") { - Task { - await model.refreshDashboard() - } - } - .disabled(model.isRefreshing) - - Button("Send Test Alert") { - Task { - await model.sendTestNotification() + .buttonStyle(.plain) } } - if model.notificationPermission == .unknown || model.notificationPermission == .denied { - Button("Enable Alerts") { + WatchSectionHeader(title: "Actions") + + VStack(alignment: .leading, spacing: 10) { + Button("Refresh") { Task { - await model.requestNotificationAccess() + await model.refreshDashboard() } } - } - } + .buttonStyle(.bordered) + .tint(watchAccent) + .disabled(model.isRefreshing) - Section("Account") { - if let profile = model.profile { - VStack(alignment: .leading, spacing: 4) { - Text(profile.handle) - .font(.headline) - Text(profile.organization) - .font(.footnote) - .foregroundStyle(.secondary) + Button("Send Test Alert") { + Task { + await model.sendTestNotification() + } } - } + .buttonStyle(.bordered) - VStack(alignment: .leading, spacing: 4) { - Text("Notifications") - .font(.headline) - Text(model.notificationPermission.title) - .font(.footnote) - .foregroundStyle(.secondary) - } + if model.notificationPermission == .unknown || model.notificationPermission == .denied { + Button("Enable Alerts") { + Task { + await model.requestNotificationAccess() + } + } + .buttonStyle(.bordered) + } - Button("Sign Out", role: .destructive) { - model.signOut() + Button("Sign Out", role: .destructive) { + model.signOut() + } + .buttonStyle(.bordered) + } + .watchCard() + + if let profile = model.profile { + WatchSectionHeader(title: "Identity") + + VStack(alignment: .leading, spacing: 8) { + Text(profile.handle) + .font(.footnote.weight(.semibold)) + .foregroundStyle(.white) + + Text(profile.organization) + .font(.caption2) + .foregroundStyle(.white.opacity(0.68)) + + Text("Notifications: \(model.notificationPermission.title)") + .font(.caption2) + .foregroundStyle(.white.opacity(0.68)) + } + .watchCard() } } + .padding(.horizontal, 8) + .padding(.top, 12) + .padding(.bottom, 20) } + .background(Color.black.ignoresSafeArea()) .navigationTitle("Passport") .refreshable { await model.refreshDashboard() @@ -201,21 +282,44 @@ private struct WatchDashboardView: View { } } +private struct WatchSectionHeader: View { + let title: String + var detail: String? = nil + + var body: some View { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(title) + .font(.headline) + .foregroundStyle(.white) + + if let detail, !detail.isEmpty { + Text(detail) + .font(.caption2.weight(.semibold)) + .foregroundStyle(.white.opacity(0.58)) + } + } + .padding(.horizontal, 2) + } +} + private struct WatchPassportCard: View { @ObservedObject var model: AppViewModel var body: some View { VStack(alignment: .leading, spacing: 10) { + AppBadge(title: "Passport active", tone: watchAccent) + VStack(alignment: .leading, spacing: 2) { Text(model.profile?.name ?? "Preview Session") .font(.headline) + .foregroundStyle(.white) Text(model.pairedDeviceSummary) .font(.footnote) - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.72)) if let session = model.session { Text("Via \(session.pairingTransport.title)") .font(.caption2) - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.58)) } } @@ -237,9 +341,10 @@ private struct WatchMetricPill: View { VStack(alignment: .leading, spacing: 2) { Text(value) .font(.headline.monospacedDigit()) + .foregroundStyle(.white) Text(title) .font(.caption2) - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.68)) } .padding(.horizontal, 10) .padding(.vertical, 8) @@ -257,6 +362,7 @@ private struct WatchRequestRow: View { Text(request.title) .font(.headline) .lineLimit(2) + .foregroundStyle(.white) Spacer(minLength: 6) @@ -266,13 +372,17 @@ private struct WatchRequestRow: View { Text(request.source) .font(.footnote) - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.72)) + + HStack(spacing: 8) { + AppStatusTag(title: request.risk.title, tone: request.risk == .routine ? watchAccent : .orange) + AppStatusTag(title: request.status.title, tone: request.status == .pending ? .orange : watchAccent) + } Text(request.createdAt.watchRelativeString) .font(.caption2) - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.58)) } - .padding(.vertical, 2) } } @@ -285,6 +395,7 @@ private struct WatchNotificationRow: View { Text(notification.title) .font(.headline) .lineLimit(2) + .foregroundStyle(.white) Spacer(minLength: 6) @@ -297,14 +408,13 @@ private struct WatchNotificationRow: View { Text(notification.message) .font(.footnote) - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.72)) .lineLimit(2) Text(notification.sentAt.watchRelativeString) .font(.caption2) - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.58)) } - .padding(.vertical, 2) } }