Polish watch companion layouts
This commit is contained in:
@@ -28,25 +28,34 @@ private struct WatchPairingView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
AppPanel(compactLayout: true, radius: 22) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
AppBadge(title: "Preview passport", tone: watchAccent)
|
AppBadge(title: "Preview passport", tone: watchAccent)
|
||||||
|
|
||||||
Text("Prove identity from your wrist")
|
Text("Prove identity from your wrist")
|
||||||
.font(.title3.weight(.semibold))
|
.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)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.white.opacity(0.72))
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
AppStatusTag(title: "Wrist-ready", tone: watchAccent)
|
AppStatusTag(title: "Wrist-ready", tone: watchAccent)
|
||||||
AppStatusTag(title: "Preview sync", tone: watchGold)
|
AppStatusTag(title: "Proof focus", tone: watchGold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.watchCard()
|
||||||
|
|
||||||
if model.isBootstrapping {
|
if model.isBootstrapping {
|
||||||
ProgressView("Preparing preview passport...")
|
HStack(spacing: 8) {
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
ProgressView()
|
||||||
|
.tint(watchAccent)
|
||||||
|
Text("Preparing preview passport...")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.white.opacity(0.72))
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.watchCard()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
@@ -58,47 +67,82 @@ private struct WatchPairingView: View {
|
|||||||
ProgressView()
|
ProgressView()
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
} else {
|
} else {
|
||||||
Label("Use Preview Passport", systemImage: "qrcode")
|
Label("Link Preview Passport", systemImage: "applewatch")
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
|
.tint(watchAccent)
|
||||||
.disabled(model.isBootstrapping || model.suggestedPairingPayload.isEmpty || model.isAuthenticating)
|
.disabled(model.isBootstrapping || model.suggestedPairingPayload.isEmpty || model.isAuthenticating)
|
||||||
|
|
||||||
AppPanel(compactLayout: true, radius: 18) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text("What works today")
|
Text("What this watch does")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
Text("The watch shows pending identity checks, recent alerts, and quick actions.")
|
WatchSetupFeatureRow(
|
||||||
.font(.footnote)
|
systemImage: "checkmark.shield",
|
||||||
.foregroundStyle(.secondary)
|
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(.horizontal, 8)
|
||||||
|
.padding(.top, 6)
|
||||||
.padding(.bottom, 20)
|
.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 title: String
|
||||||
let value: String
|
let subtitle: String
|
||||||
let tone: Color
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
HStack(alignment: .top, spacing: 10) {
|
||||||
Text(title)
|
Image(systemName: systemImage)
|
||||||
.font(.caption2)
|
.font(.footnote.weight(.semibold))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(watchAccent)
|
||||||
Text(value)
|
.frame(width: 18, height: 18)
|
||||||
.font(.caption.weight(.semibold))
|
|
||||||
.foregroundStyle(.primary)
|
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
|
@ObservedObject var model: AppViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
ScrollView {
|
||||||
Section {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
WatchPassportCard(model: model)
|
WatchPassportCard(model: model)
|
||||||
}
|
.watchCard()
|
||||||
|
|
||||||
|
WatchSectionHeader(
|
||||||
|
title: "Pending",
|
||||||
|
detail: model.pendingRequests.isEmpty ? nil : "\(model.pendingRequests.count)"
|
||||||
|
)
|
||||||
|
|
||||||
Section("Pending") {
|
|
||||||
if model.pendingRequests.isEmpty {
|
if model.pendingRequests.isEmpty {
|
||||||
Text("No checks waiting.")
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
.foregroundStyle(.secondary)
|
Text("No checks waiting.")
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
Button("Seed Identity Check") {
|
Text("New identity checks will appear here when a site or device asks you to prove it is really you.")
|
||||||
Task {
|
.font(.caption2)
|
||||||
await model.simulateIncomingRequest()
|
.foregroundStyle(.white.opacity(0.68))
|
||||||
|
|
||||||
|
Button("Seed Identity Check") {
|
||||||
|
Task {
|
||||||
|
await model.simulateIncomingRequest()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.tint(watchAccent)
|
||||||
}
|
}
|
||||||
|
.watchCard()
|
||||||
} else {
|
} else {
|
||||||
ForEach(model.pendingRequests) { request in
|
ForEach(model.pendingRequests) { request in
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
WatchRequestDetailView(model: model, requestID: request.id)
|
WatchRequestDetailView(model: model, requestID: request.id)
|
||||||
} label: {
|
} label: {
|
||||||
WatchRequestRow(request: request)
|
WatchRequestRow(request: request)
|
||||||
|
.watchCard()
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Section("Recent Activity") {
|
WatchSectionHeader(title: "Activity")
|
||||||
|
|
||||||
if model.notifications.isEmpty {
|
if model.notifications.isEmpty {
|
||||||
Text("No recent alerts.")
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
.foregroundStyle(.secondary)
|
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 {
|
} else {
|
||||||
ForEach(model.notifications.prefix(3)) { notification in
|
ForEach(model.notifications.prefix(3)) { notification in
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
WatchNotificationDetailView(model: model, notificationID: notification.id)
|
WatchNotificationDetailView(model: model, notificationID: notification.id)
|
||||||
} label: {
|
} label: {
|
||||||
WatchNotificationRow(notification: notification)
|
WatchNotificationRow(notification: notification)
|
||||||
|
.watchCard()
|
||||||
}
|
}
|
||||||
}
|
.buttonStyle(.plain)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section("Actions") {
|
|
||||||
Button("Refresh") {
|
|
||||||
Task {
|
|
||||||
await model.refreshDashboard()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.disabled(model.isRefreshing)
|
|
||||||
|
|
||||||
Button("Send Test Alert") {
|
|
||||||
Task {
|
|
||||||
await model.sendTestNotification()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if model.notificationPermission == .unknown || model.notificationPermission == .denied {
|
WatchSectionHeader(title: "Actions")
|
||||||
Button("Enable Alerts") {
|
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Button("Refresh") {
|
||||||
Task {
|
Task {
|
||||||
await model.requestNotificationAccess()
|
await model.refreshDashboard()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.buttonStyle(.bordered)
|
||||||
}
|
.tint(watchAccent)
|
||||||
|
.disabled(model.isRefreshing)
|
||||||
|
|
||||||
Section("Account") {
|
Button("Send Test Alert") {
|
||||||
if let profile = model.profile {
|
Task {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
await model.sendTestNotification()
|
||||||
Text(profile.handle)
|
}
|
||||||
.font(.headline)
|
|
||||||
Text(profile.organization)
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
}
|
||||||
}
|
.buttonStyle(.bordered)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
if model.notificationPermission == .unknown || model.notificationPermission == .denied {
|
||||||
Text("Notifications")
|
Button("Enable Alerts") {
|
||||||
.font(.headline)
|
Task {
|
||||||
Text(model.notificationPermission.title)
|
await model.requestNotificationAccess()
|
||||||
.font(.footnote)
|
}
|
||||||
.foregroundStyle(.secondary)
|
}
|
||||||
}
|
.buttonStyle(.bordered)
|
||||||
|
}
|
||||||
|
|
||||||
Button("Sign Out", role: .destructive) {
|
Button("Sign Out", role: .destructive) {
|
||||||
model.signOut()
|
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")
|
.navigationTitle("Passport")
|
||||||
.refreshable {
|
.refreshable {
|
||||||
await model.refreshDashboard()
|
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 {
|
private struct WatchPassportCard: View {
|
||||||
@ObservedObject var model: AppViewModel
|
@ObservedObject var model: AppViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
AppBadge(title: "Passport active", tone: watchAccent)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(model.profile?.name ?? "Preview Session")
|
Text(model.profile?.name ?? "Preview Session")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
.foregroundStyle(.white)
|
||||||
Text(model.pairedDeviceSummary)
|
Text(model.pairedDeviceSummary)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.white.opacity(0.72))
|
||||||
if let session = model.session {
|
if let session = model.session {
|
||||||
Text("Via \(session.pairingTransport.title)")
|
Text("Via \(session.pairingTransport.title)")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.white.opacity(0.58))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,9 +341,10 @@ private struct WatchMetricPill: View {
|
|||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(value)
|
Text(value)
|
||||||
.font(.headline.monospacedDigit())
|
.font(.headline.monospacedDigit())
|
||||||
|
.foregroundStyle(.white)
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.white.opacity(0.68))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
@@ -257,6 +362,7 @@ private struct WatchRequestRow: View {
|
|||||||
Text(request.title)
|
Text(request.title)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
Spacer(minLength: 6)
|
Spacer(minLength: 6)
|
||||||
|
|
||||||
@@ -266,13 +372,17 @@ private struct WatchRequestRow: View {
|
|||||||
|
|
||||||
Text(request.source)
|
Text(request.source)
|
||||||
.font(.footnote)
|
.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)
|
Text(request.createdAt.watchRelativeString)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.white.opacity(0.58))
|
||||||
}
|
}
|
||||||
.padding(.vertical, 2)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,6 +395,7 @@ private struct WatchNotificationRow: View {
|
|||||||
Text(notification.title)
|
Text(notification.title)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
Spacer(minLength: 6)
|
Spacer(minLength: 6)
|
||||||
|
|
||||||
@@ -297,14 +408,13 @@ private struct WatchNotificationRow: View {
|
|||||||
|
|
||||||
Text(notification.message)
|
Text(notification.message)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.white.opacity(0.72))
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
|
|
||||||
Text(notification.sentAt.watchRelativeString)
|
Text(notification.sentAt.watchRelativeString)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.white.opacity(0.58))
|
||||||
}
|
}
|
||||||
.padding(.vertical, 2)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user