Refine home dashboard passport layout
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user