Refine home dashboard passport layout

This commit is contained in:
2026-04-17 22:29:16 +02:00
parent 6936ad5cfe
commit eb0f9e2e7a

View File

@@ -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<IDP\(session.pairingCode.uppercased())<\(normalizedName)<<\(normalizedHandle)<<\(normalizedToken)"
let normalizedOrigin = sanitize(session.originHost)
return "P<\(documentNumber)<\(normalizedName)<<\(normalizedHandle)<<\(normalizedOrigin)"
}
private func sanitize(_ value: String) -> 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<Content: View>: 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<Content: View>: 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()
}
}