diff --git a/Sources/Features/Home/HomeRootView.swift b/Sources/Features/Home/HomeRootView.swift index 5860055..5c1051b 100644 --- a/Sources/Features/Home/HomeRootView.swift +++ b/Sources/Features/Home/HomeRootView.swift @@ -2,17 +2,54 @@ import SwiftUI private let dashboardAccent = Color(red: 0.12, green: 0.40, blue: 0.31) private let dashboardGold = Color(red: 0.84, green: 0.71, blue: 0.48) +private let dashboardBorder = Color.black.opacity(0.06) +private let dashboardShadow = Color.black.opacity(0.05) + +private enum DashboardSpacing { + static let compactOuterPadding: CGFloat = 16 + static let regularOuterPadding: CGFloat = 28 + static let compactTopPadding: CGFloat = 10 + static let regularTopPadding: CGFloat = 18 + static let compactBottomPadding: CGFloat = 120 + static let regularBottomPadding: CGFloat = 56 + static let compactStackSpacing: CGFloat = 20 + static let regularStackSpacing: CGFloat = 28 + static let compactContentWidth: CGFloat = 720 + static let regularContentWidth: CGFloat = 980 + static let compactSectionPadding: CGFloat = 18 + static let regularSectionPadding: CGFloat = 24 + static let compactRadius: CGFloat = 24 + static let regularRadius: CGFloat = 28 +} + +private extension View { + func dashboardSurface(radius: CGFloat, fillOpacity: Double = 0.88) -> some View { + background( + Color.white.opacity(fillOpacity), + in: RoundedRectangle(cornerRadius: radius, style: .continuous) + ) + .overlay( + RoundedRectangle(cornerRadius: radius, style: .continuous) + .stroke(dashboardBorder, lineWidth: 1) + ) + .shadow(color: dashboardShadow, radius: 14, y: 6) + } +} struct HomeRootView: View { @ObservedObject var model: AppViewModel @Environment(\.horizontalSizeClass) private var horizontalSizeClass var body: some View { - Group { - if usesCompactNavigation { - CompactHomeContainer(model: model) - } else { - RegularHomeContainer(model: model) + ZStack { + DashboardBackdrop() + + Group { + if usesCompactNavigation { + CompactHomeContainer(model: model) + } else { + RegularHomeContainer(model: model) + } } } .sheet(isPresented: $model.isNotificationCenterPresented) { @@ -153,7 +190,7 @@ private struct HomeSectionScreen: View { var body: some View { ScrollView { - VStack(alignment: .leading, spacing: compactLayout ? 18 : 24) { + VStack(alignment: .leading, spacing: compactLayout ? DashboardSpacing.compactStackSpacing : DashboardSpacing.regularStackSpacing) { if let banner = model.bannerMessage { BannerCard(message: banner, compactLayout: compactLayout) } @@ -181,8 +218,11 @@ private struct HomeSectionScreen: View { AccountPanel(model: model, compactLayout: compactLayout) } } - .padding(compactLayout ? 18 : 24) - .frame(maxWidth: compactLayout ? 720 : 1120, alignment: .leading) + .padding(.horizontal, compactLayout ? DashboardSpacing.compactOuterPadding : DashboardSpacing.regularOuterPadding) + .padding(.top, compactLayout ? DashboardSpacing.compactTopPadding : DashboardSpacing.regularTopPadding) + .padding(.bottom, compactLayout ? DashboardSpacing.compactBottomPadding : DashboardSpacing.regularBottomPadding) + .frame(maxWidth: compactLayout ? DashboardSpacing.compactContentWidth : DashboardSpacing.regularContentWidth, alignment: .leading) + .frame(maxWidth: .infinity, alignment: compactLayout ? .leading : .center) } .scrollIndicators(.hidden) .sheet(item: $focusedRequest) { request in @@ -453,7 +493,7 @@ private struct ActivityPanel: View { } } } - .frame(maxWidth: 340, alignment: .leading) + .frame(width: 390, alignment: .leading) .padding(18) .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 28, style: .continuous)) @@ -687,7 +727,7 @@ private struct RequestsPanel: View { } } } - .frame(maxWidth: 340, alignment: .leading) + .frame(width: 390, alignment: .leading) .padding(18) .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 28, style: .continuous)) @@ -995,115 +1035,149 @@ private struct OverviewHero: View { .offset(x: compactLayout ? 220 : 455, y: compactLayout ? 4 : 8) VStack(alignment: .leading, spacing: compactLayout ? 16 : 20) { - HStack(alignment: .top, spacing: 12) { - VStack(alignment: .leading, spacing: 6) { - Text("IDP.GLOBAL DIGITAL PASSPORT") - .font(.caption.weight(.bold)) - .tracking(1.8) - .foregroundStyle(.white.opacity(0.78)) + passportHeader - Text(profile.name) - .font(.system(size: compactLayout ? 30 : 36, weight: .bold, design: .rounded)) - .foregroundStyle(.white) + passportBody - Text("Bound to \(session.deviceName) for requests coming from \(session.originHost).") - .font(compactLayout ? .subheadline : .title3) - .foregroundStyle(.white.opacity(0.88)) - } - - Spacer(minLength: 0) - - VStack(alignment: .trailing, spacing: 8) { - StatusBadge(title: "Bound", tone: .white) - - Text(session.pairingCode) - .font(.caption.monospaced().weight(.semibold)) - .padding(.horizontal, 10) - .padding(.vertical, 7) - .background(.white.opacity(0.12), in: Capsule()) - .foregroundStyle(.white) - } + if !compactLayout { + PassportMachineStrip(code: machineReadableCode) } - Group { - if compactLayout { - VStack(alignment: .leading, spacing: 14) { - passportPortrait - - VStack(spacing: 12) { - passportPrimaryFields - passportSecondaryFields - } - } - } else { - HStack(alignment: .top, spacing: 18) { - passportPortrait - - HStack(alignment: .top, spacing: 14) { - passportPrimaryFields - passportSecondaryFields - } - } - } - } - .padding(compactLayout ? 18 : 20) - .background(.white.opacity(0.11), in: RoundedRectangle(cornerRadius: 28, style: .continuous)) - - PassportMachineStrip(code: machineReadableCode) - - if compactLayout { - VStack(spacing: 10) { - PassportMetricBadge( - title: "Pending", - value: "\(pendingCount)", - subtitle: pendingCount == 0 ? "No approvals waiting" : "Requests still at the border" - ) - PassportMetricBadge( - title: "Alerts", - value: "\(unreadCount)", - subtitle: unreadCount == 0 ? "Notification bell is clear" : "Unread device alerts" - ) - PassportMetricBadge( - title: "Devices", - value: "\(profile.deviceCount)", - subtitle: "\(profile.organization) membership" - ) - } - } else { - HStack(spacing: 12) { - PassportMetricBadge( - title: "Pending", - value: "\(pendingCount)", - subtitle: pendingCount == 0 ? "No approvals waiting" : "Requests still at the border" - ) - PassportMetricBadge( - title: "Alerts", - value: "\(unreadCount)", - subtitle: unreadCount == 0 ? "Notification bell is clear" : "Unread device alerts" - ) - PassportMetricBadge( - title: "Devices", - value: "\(profile.deviceCount)", - subtitle: "\(profile.organization) membership" - ) - } - } + passportMetrics } .padding(compactLayout ? 22 : 28) } - .frame(minHeight: compactLayout ? 470 : 390) + .frame(minHeight: compactLayout ? 380 : 390) + } + + private var passportHeader: some View { + HStack(alignment: .top, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("IDP.GLOBAL DIGITAL PASSPORT") + .font(.caption.weight(.bold)) + .tracking(1.8) + .foregroundStyle(.white.opacity(0.78)) + + Text(profile.name) + .font(.system(size: compactLayout ? 30 : 36, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + + Text("Bound to \(session.deviceName) for requests coming from \(session.originHost).") + .font(compactLayout ? .subheadline : .title3) + .foregroundStyle(.white.opacity(0.88)) + } + + Spacer(minLength: 0) + + PassportDocumentBadge( + number: documentNumber, + issuedAt: session.pairedAt, + compactLayout: compactLayout + ) + } + } + + @ViewBuilder + private var passportBody: some View { + if compactLayout { + VStack(alignment: .leading, spacing: 14) { + HStack(alignment: .top, spacing: 14) { + passportPortrait + + VStack(alignment: .leading, spacing: 10) { + PassportField(label: "Holder", value: profile.name, emphasized: true) + PassportField(label: "Handle", value: profile.handle, monospaced: true) + PassportField(label: "Origin", value: session.originHost, monospaced: true) + } + } + + LazyVGrid( + columns: [ + GridItem(.flexible(), spacing: 10), + GridItem(.flexible(), spacing: 10) + ], + spacing: 10 + ) { + PassportInlineFact(label: "Device", value: session.deviceName) + PassportInlineFact(label: "Issued", value: session.pairedAt.formatted(date: .abbreviated, time: .shortened)) + PassportInlineFact(label: "Organization", value: profile.organization) + PassportInlineFact(label: "Token", value: "...\(session.tokenPreview)", monospaced: true) + } + } + .padding(18) + .background(.white.opacity(0.11), in: RoundedRectangle(cornerRadius: 28, style: .continuous)) + } else { + VStack(alignment: .leading, spacing: 16) { + HStack(alignment: .top, spacing: 18) { + passportPortrait + + HStack(alignment: .top, spacing: 14) { + passportPrimaryFields + passportSecondaryFields + } + } + + LazyVGrid( + columns: [ + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12), + GridItem(.flexible(), spacing: 12) + ], + spacing: 12 + ) { + PassportInlineFact(label: "Document No.", value: documentNumber, monospaced: true) + PassportInlineFact(label: "Issued", value: session.pairedAt.formatted(date: .abbreviated, time: .shortened)) + PassportInlineFact(label: "Membership", value: "\(profile.deviceCount) trusted devices") + } + } + .padding(20) + .background(.white.opacity(0.11), in: RoundedRectangle(cornerRadius: 28, style: .continuous)) + } + } + + private var passportMetrics: some View { + Group { + if compactLayout { + VStack(spacing: 10) { + passportMetricCards + } + } else { + HStack(spacing: 12) { + passportMetricCards + } + } + } + } + + @ViewBuilder + private var passportMetricCards: some View { + PassportMetricBadge( + title: "Pending", + value: "\(pendingCount)", + subtitle: pendingCount == 0 ? "No approvals waiting" : "Requests still at the border" + ) + PassportMetricBadge( + title: "Alerts", + value: "\(unreadCount)", + subtitle: unreadCount == 0 ? "Notification bell is clear" : "Unread device alerts" + ) + PassportMetricBadge( + title: "Devices", + value: "\(profile.deviceCount)", + subtitle: "\(profile.organization) membership" + ) } private var passportPortrait: some View { VStack(alignment: .leading, spacing: 12) { RoundedRectangle(cornerRadius: 26, style: .continuous) .fill(.white.opacity(0.12)) - .frame(width: compactLayout ? 118 : 132, height: compactLayout ? 148 : 166) + .frame(width: compactLayout ? 102 : 132, height: compactLayout ? 132 : 166) .overlay { VStack(spacing: 10) { Circle() .fill(.white.opacity(0.18)) - .frame(width: compactLayout ? 56 : 64, height: compactLayout ? 56 : 64) + .frame(width: compactLayout ? 52 : 64, height: compactLayout ? 52 : 64) .overlay { Text(holderInitials) .font(.system(size: compactLayout ? 24 : 28, weight: .bold, design: .rounded)) @@ -1115,9 +1189,11 @@ private struct OverviewHero: View { .tracking(1.2) .foregroundStyle(.white.opacity(0.72)) - Text(profile.handle) + Text(compactLayout ? documentNumber : profile.handle) .font(.footnote.monospaced()) .foregroundStyle(.white.opacity(0.9)) + .lineLimit(2) + .minimumScaleFactor(0.7) } .padding(12) } @@ -1154,11 +1230,15 @@ private struct OverviewHero: View { return initials.isEmpty ? "ID" : initials.uppercased() } + private var documentNumber: String { + "IDP-\(session.id.uuidString.prefix(8).uppercased())" + } + private var machineReadableCode: String { let normalizedName = sanitize(profile.name) let normalizedHandle = sanitize(profile.handle) - let normalizedToken = sanitize(session.tokenPreview) - return "P String { @@ -1171,6 +1251,62 @@ private struct OverviewHero: View { } } +private struct PassportDocumentBadge: View { + let number: String + let issuedAt: Date + let compactLayout: Bool + + var body: some View { + VStack(alignment: .trailing, spacing: 8) { + StatusBadge(title: "Bound", tone: .white) + + VStack(alignment: .trailing, spacing: 4) { + Text("Document No.") + .font(.caption2.weight(.bold)) + .tracking(1.0) + .foregroundStyle(.white.opacity(0.72)) + + Text(number) + .font((compactLayout ? Font.footnote : Font.body).monospaced().weight(.semibold)) + .foregroundStyle(.white) + } + + if !compactLayout { + Text("Issued \(issuedAt.formatted(date: .abbreviated, time: .shortened))") + .font(.caption) + .foregroundStyle(.white.opacity(0.76)) + } + } + .padding(.horizontal, compactLayout ? 12 : 14) + .padding(.vertical, 10) + .background(.white.opacity(0.10), in: RoundedRectangle(cornerRadius: 22, style: .continuous)) + } +} + +private struct PassportInlineFact: View { + let label: String + let value: String + var monospaced: Bool = false + + var body: some View { + VStack(alignment: .leading, spacing: 5) { + Text(label.uppercased()) + .font(.caption2.weight(.bold)) + .tracking(1.0) + .foregroundStyle(.white.opacity(0.72)) + + Text(value) + .font(monospaced ? .subheadline.monospaced() : .subheadline.weight(.semibold)) + .foregroundStyle(.white) + .lineLimit(2) + .minimumScaleFactor(0.7) + } + .padding(12) + .frame(maxWidth: .infinity, alignment: .leading) + .background(.white.opacity(0.09), in: RoundedRectangle(cornerRadius: 18, style: .continuous)) + } +} + private struct PassportField: View { let label: String let value: String @@ -1306,8 +1442,10 @@ private struct ActionTile: View { Button(action: action) { VStack(alignment: .leading, spacing: 12) { Image(systemName: systemImage) - .font(.title2) + .font(.title3.weight(.semibold)) .foregroundStyle(dashboardAccent) + .frame(width: 42, height: 42) + .background(dashboardAccent.opacity(0.10), in: RoundedRectangle(cornerRadius: 14, style: .continuous)) Text(title) .font(.headline) .foregroundStyle(.primary) @@ -1317,7 +1455,11 @@ private struct ActionTile: View { } .frame(maxWidth: .infinity, alignment: .leading) .padding(18) - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 24, style: .continuous)) + .background(Color.white.opacity(0.76), in: RoundedRectangle(cornerRadius: 24, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 24, style: .continuous) + .stroke(dashboardAccent.opacity(0.08), lineWidth: 1) + ) } .buttonStyle(.plain) } @@ -1400,35 +1542,43 @@ private struct RequestQueueSummary: View { let compactLayout: Bool var body: some View { - Group { - if compactLayout { - VStack(spacing: 12) { - summaryCards - } - } else { + if compactLayout { + VStack(spacing: 12) { HStack(spacing: 12) { - summaryCards + pendingCard + elevatedCard } + + postureCard + } + } else { + HStack(spacing: 12) { + pendingCard + elevatedCard + postureCard } } } - @ViewBuilder - private var summaryCards: some View { + private var pendingCard: some View { RequestSummaryMetricCard( title: "Pending", value: "\(pendingCount)", subtitle: pendingCount == 0 ? "Queue is clear" : "Still waiting on your call", accent: dashboardAccent ) + } + private var elevatedCard: some View { RequestSummaryMetricCard( title: "Elevated", value: "\(elevatedCount)", subtitle: elevatedCount == 0 ? "No privileged scopes" : "Needs slower review", accent: .orange ) + } + private var postureCard: some View { RequestSummaryMetricCard( title: "Posture", value: trustMode, @@ -1484,7 +1634,11 @@ private struct RequestSummaryMetricCard: View { } .padding(16) .frame(maxWidth: .infinity, alignment: .leading) - .background(accent.opacity(0.12), in: RoundedRectangle(cornerRadius: 22, style: .continuous)) + .background(accent.opacity(0.10), in: RoundedRectangle(cornerRadius: 20, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 20, style: .continuous) + .stroke(accent.opacity(0.08), lineWidth: 1) + ) } } @@ -1522,7 +1676,7 @@ private struct NotificationPermissionCard: View { } } .padding(18) - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 26, style: .continuous)) + .dashboardSurface(radius: 24) } @ViewBuilder @@ -1650,8 +1804,11 @@ private struct NotificationCenterSheet: View { NavigationStack { ScrollView { NotificationsPanel(model: model, compactLayout: compactLayout) - .padding(compactLayout ? 18 : 24) - .frame(maxWidth: compactLayout ? 720 : 1120, alignment: .leading) + .padding(.horizontal, compactLayout ? DashboardSpacing.compactOuterPadding : DashboardSpacing.regularOuterPadding) + .padding(.top, compactLayout ? DashboardSpacing.compactTopPadding : DashboardSpacing.regularTopPadding) + .padding(.bottom, compactLayout ? DashboardSpacing.compactBottomPadding : DashboardSpacing.regularBottomPadding) + .frame(maxWidth: compactLayout ? DashboardSpacing.compactContentWidth : DashboardSpacing.regularContentWidth, alignment: .leading) + .frame(maxWidth: .infinity, alignment: compactLayout ? .leading : .center) } .scrollIndicators(.hidden) .navigationTitle("Notifications") @@ -1817,6 +1974,10 @@ private struct RequestCard: View { .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background(requestAccent.opacity(0.10), in: RoundedRectangle(cornerRadius: 22, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 22, style: .continuous) + .stroke(requestAccent.opacity(0.08), lineWidth: 1) + ) if !request.scopes.isEmpty { VStack(alignment: .leading, spacing: 10) { @@ -1891,8 +2052,8 @@ private struct RequestCard: View { .background( LinearGradient( colors: [ - requestAccent.opacity(0.10), - Color.white.opacity(0.74) + Color.white.opacity(0.92), + requestAccent.opacity(0.05) ], startPoint: .topLeading, endPoint: .bottomTrailing @@ -1901,8 +2062,9 @@ private struct RequestCard: View { ) .overlay( RoundedRectangle(cornerRadius: 28, style: .continuous) - .stroke(requestAccent.opacity(0.16), lineWidth: 1) + .stroke(requestAccent.opacity(0.10), lineWidth: 1) ) + .shadow(color: dashboardShadow, radius: 12, y: 5) } private var statusTone: Color { @@ -1967,10 +2129,12 @@ private struct RequestQueueRow: View { .font(.headline) .foregroundStyle(.primary) .multilineTextAlignment(.leading) + .lineLimit(2) Text(request.trustHeadline) .font(.subheadline.weight(.semibold)) .foregroundStyle(rowAccent) + .lineLimit(1) } Spacer(minLength: 0) @@ -1984,11 +2148,12 @@ private struct RequestQueueRow: View { Text(request.source) .font(.subheadline) .foregroundStyle(.secondary) + .lineLimit(1) Text(request.subtitle) .font(.footnote) .foregroundStyle(.secondary) - .lineLimit(2) + .lineLimit(1) .multilineTextAlignment(.leading) HStack(spacing: 8) { @@ -2011,8 +2176,15 @@ private struct RequestQueueRow: View { .background(backgroundStyle, in: RoundedRectangle(cornerRadius: 24, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 24, style: .continuous) - .stroke(isSelected ? dashboardAccent.opacity(0.36) : Color.clear, lineWidth: 1.5) + .stroke(isSelected ? rowAccent.opacity(0.36) : Color.clear, lineWidth: 1.5) ) + .overlay(alignment: .leading) { + Capsule() + .fill(rowAccent.opacity(isSelected ? 0.80 : 0.30)) + .frame(width: 5) + .padding(.vertical, 16) + .padding(.leading, 8) + } } .buttonStyle(.plain) } @@ -2029,7 +2201,7 @@ private struct RequestQueueRow: View { } private var backgroundStyle: Color { - isSelected ? rowAccent.opacity(0.12) : Color.white.opacity(0.58) + isSelected ? rowAccent.opacity(0.08) : Color.white.opacity(0.90) } private var rowAccent: Color { @@ -2058,7 +2230,7 @@ private struct RequestWorkbenchDetail: View { var body: some View { VStack(alignment: .leading, spacing: 18) { - ZStack(alignment: .bottomLeading) { + ZStack(alignment: .topLeading) { RoundedRectangle(cornerRadius: 30, style: .continuous) .fill( LinearGradient( @@ -2075,7 +2247,7 @@ private struct RequestWorkbenchDetail: View { .strokeBorder(requestAccent.opacity(0.20), lineWidth: 1) ) - VStack(alignment: .leading, spacing: 14) { + VStack(alignment: .leading, spacing: 16) { HStack(alignment: .top, spacing: 12) { VStack(alignment: .leading, spacing: 10) { HStack(spacing: 8) { @@ -2107,6 +2279,7 @@ private struct RequestWorkbenchDetail: View { } Text(request.subtitle) + .font(.title3) .foregroundStyle(.white.opacity(0.88)) HStack(spacing: 14) { @@ -2115,10 +2288,14 @@ private struct RequestWorkbenchDetail: View { } .font(.subheadline) .foregroundStyle(.white.opacity(0.88)) + + Text(request.trustDetail) + .font(.subheadline) + .foregroundStyle(.white.opacity(0.82)) } .padding(24) } - .frame(minHeight: 250) + .frame(minHeight: 220) LazyVGrid(columns: columns, spacing: 12) { FactCard(label: "Source", value: request.source) @@ -2270,6 +2447,10 @@ private struct RequestFactPill: View { .padding(.vertical, 10) .frame(maxWidth: .infinity, alignment: .leading) .background(accent.opacity(0.10), in: RoundedRectangle(cornerRadius: 18, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 18, style: .continuous) + .stroke(accent.opacity(0.08), lineWidth: 1) + ) } } @@ -2314,7 +2495,7 @@ private struct RequestSignalCard: View { } .padding(18) .frame(maxWidth: .infinity, alignment: .leading) - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 26, style: .continuous)) + .dashboardSurface(radius: 24) } } @@ -2395,7 +2576,11 @@ private struct RequestDetailSheet: View { } } } - .padding(20) + .padding(.horizontal, DashboardSpacing.compactOuterPadding) + .padding(.top, DashboardSpacing.compactTopPadding) + .padding(.bottom, DashboardSpacing.compactBottomPadding) + .frame(maxWidth: DashboardSpacing.compactContentWidth, alignment: .leading) + .frame(maxWidth: .infinity, alignment: .leading) } .navigationTitle("Review Request") .toolbar { @@ -2495,7 +2680,7 @@ private struct NotificationCard: View { } } .padding(compactLayout ? 16 : 18) - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 26, style: .continuous)) + .dashboardSurface(radius: compactLayout ? 22 : 24) } private var timestampLabel: some View { @@ -2547,7 +2732,11 @@ private struct NotificationMetricCard: View { } .padding(18) .frame(maxWidth: .infinity, alignment: .leading) - .background(accent.opacity(0.12), in: RoundedRectangle(cornerRadius: 24, style: .continuous)) + .background(accent.opacity(0.10), in: RoundedRectangle(cornerRadius: 20, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 20, style: .continuous) + .stroke(accent.opacity(0.08), lineWidth: 1) + ) } } @@ -2744,9 +2933,9 @@ private struct SectionCard: View { content() } - .padding(compactLayout ? 18 : 24) + .padding(compactLayout ? DashboardSpacing.compactSectionPadding : DashboardSpacing.regularSectionPadding) .frame(maxWidth: .infinity, alignment: .leading) - .background(Color.white.opacity(compactLayout ? 0.78 : 0.68), in: RoundedRectangle(cornerRadius: 32, style: .continuous)) + .dashboardSurface(radius: compactLayout ? DashboardSpacing.compactRadius : DashboardSpacing.regularRadius) } } @@ -2762,9 +2951,9 @@ private struct BannerCard: View { Text(message) .font(compactLayout ? .subheadline.weight(.semibold) : .headline) } - .padding(.horizontal, 18) - .padding(.vertical, 14) - .background(.ultraThinMaterial, in: Capsule()) + .padding(.horizontal, 16) + .padding(.vertical, 12) + .dashboardSurface(radius: 999, fillOpacity: 0.84) } } @@ -2899,7 +3088,7 @@ private struct FactCard: View { } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) + .dashboardSurface(radius: 18) } } @@ -2910,9 +3099,41 @@ private struct StatusBadge: View { var body: some View { Text(title) .font(.caption.weight(.semibold)) + .lineLimit(1) + .minimumScaleFactor(0.8) + .fixedSize(horizontal: true, vertical: false) .padding(.horizontal, 10) .padding(.vertical, 6) .background(tone.opacity(0.14), in: Capsule()) .foregroundStyle(tone) } } + +private struct DashboardBackdrop: View { + var body: some View { + LinearGradient( + colors: [ + Color(red: 0.98, green: 0.98, blue: 0.97), + Color.white, + Color(red: 0.97, green: 0.98, blue: 0.99) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .overlay(alignment: .topLeading) { + Circle() + .fill(dashboardAccent.opacity(0.10)) + .frame(width: 360, height: 360) + .blur(radius: 70) + .offset(x: -120, y: -120) + } + .overlay(alignment: .bottomTrailing) { + Circle() + .fill(dashboardGold.opacity(0.12)) + .frame(width: 420, height: 420) + .blur(radius: 90) + .offset(x: 140, y: 160) + } + .ignoresSafeArea() + } +}