From 9e58a36915bb1fe8cf88423c9519c0c12dbbf2e2 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Mon, 20 Jul 2026 20:25:01 -0500 Subject: fix(a11y): rebalance the light palette so hues survive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported as "colors seem muted and hard to see on light mode", and correct. The light palette optimised contrast and produced mud: #7A5600 reads olive rather than amber, #146C2E bottle-dark rather than green. Contrast passed while the UI got harder to read, because hue identity is what distinguishes warning from critical at a glance. Two causes, both fixed. Every foreground was required to clear 4.5:1 against its own 16% badge tint — the harshest surface it ever sits on — which pushed each colour about 20% darker than the common case needed. Most of what is actually on screen is plain text on a card, with far more headroom. The fill is now decoupled from the foreground: AppStatusTone carries a foreground and a surface authored independently, with matching …Surface colorsets, so a foreground no longer has to survive a wash of itself. Every status foreground is now fully saturated. And warning was yellow. Yellow cannot stay yellow at a lightness low enough to pass 4.5:1 on white — it becomes olive. That is colorimetric, not a tuning problem. Warning is now orange: #AD5100 light, #FF9F0A dark. New light values: positive #008035, warning #AD5100, critical #CC0700. Worst-case ratios 4.54–6.76 across page, card, and surface in both schemes. Audit findings are unchanged — light 21, dark 18 — so the vividness costs nothing. Also picks up a literal .blue missed in phase 1: DomainDiffItem's low-severity change colour, which the phase 1 sweep did not cover because its pattern listed only cyan/yellow/green/red/orange/pink. --- Docs/ACCESSIBILITY.md | 36 ++++++++-- DomainDig/BatchResultsView.swift | 16 ++--- DomainDig/ContentView.swift | 60 ++++++++--------- DomainDig/DashboardView.swift | 24 +++---- DomainDig/DomainDig/AuditViews.swift | 20 +++--- DomainDig/DomainDigUI.swift | 63 ++++++++++++++--- DomainDig/HistoryView.swift | 2 +- DomainDig/MonitoringView.swift | 10 +-- DomainDig/TimelineView.swift | 2 +- DomainDig/WatchlistView.swift | 8 +-- .../StatusCritical.colorset/Contents.json | 12 ++-- .../StatusCriticalSurface.colorset/Contents.json | 78 ++++++++++++++++++++++ .../StatusInfoSurface.colorset/Contents.json | 78 ++++++++++++++++++++++ .../StatusNeutralSurface.colorset/Contents.json | 78 ++++++++++++++++++++++ .../StatusPositive.colorset/Contents.json | 12 ++-- .../StatusPositiveSurface.colorset/Contents.json | 78 ++++++++++++++++++++++ .../StatusWarning.colorset/Contents.json | 14 ++-- .../StatusWarningSurface.colorset/Contents.json | 78 ++++++++++++++++++++++ 18 files changed, 566 insertions(+), 103 deletions(-) create mode 100644 Shared/Colors.xcassets/StatusCriticalSurface.colorset/Contents.json create mode 100644 Shared/Colors.xcassets/StatusInfoSurface.colorset/Contents.json create mode 100644 Shared/Colors.xcassets/StatusNeutralSurface.colorset/Contents.json create mode 100644 Shared/Colors.xcassets/StatusPositiveSurface.colorset/Contents.json create mode 100644 Shared/Colors.xcassets/StatusWarningSurface.colorset/Contents.json diff --git a/Docs/ACCESSIBILITY.md b/Docs/ACCESSIBILITY.md index 7153381..40acc1c 100644 --- a/Docs/ACCESSIBILITY.md +++ b/Docs/ACCESSIBILITY.md @@ -24,11 +24,37 @@ worst of those three is shown: | Role | Light | Dark | Worst light | Worst dark | | --- | --- | --- | --- | --- | -| `StatusInfo` / `AccentColor` | `#0000FF` | `#4DA3FF` | 5.50 | 5.85 | -| `StatusPositive` | `#146C2E` | `#30D158` | 4.65 | 7.32 | -| `StatusWarning` | `#7A5600` | `#FFD60A` | 4.74 | 9.61 | -| `StatusCritical` | `#B3261E` | `#FF6961` | 4.51 | 5.54 | -| `StatusNeutral` | `#5A5A5F` | `#A1A1A6` | 4.92 | 5.88 | +| `StatusInfo` / `AccentColor` | `#0000FF` | `#4DA3FF` | 6.76 | 6.47 | +| `StatusPositive` | `#008035` | `#30D158` | 4.54 | 7.62 | +| `StatusWarning` | `#AD5100` | `#FF9F0A` | 4.59 | 7.76 | +| `StatusCritical` | `#CC0700` | `#FF6961` | 4.68 | 6.12 | +| `StatusNeutral` | `#5A5A5F` | `#A1A1A6` | 5.84 | 6.76 | + +Each status foreground has a matching `…Surface` colour for the fill behind it, +paired through `AppStatusTone`. + +### Contrast alone is not a palette + +The first version of this palette maximised contrast and produced mud. Requiring +every foreground to clear 4.5:1 against *its own 16% tint* — the harshest +surface it ever sits on — pushed each colour ~20% darker than the common case +needed. `#7A5600` is not amber, it is olive; `#146C2E` is not green so much as +bottle-dark. Contrast passed and the UI was still hard to read, because hue +identity is what tells "warning" from "critical" at a glance. + +Two fixes: + +1. **Decouple the fill from the foreground.** `AppStatusTone` carries a + `foreground` and a `surface` that are authored independently, so the + foreground no longer has to survive a wash of itself. Every status foreground + is now fully saturated (`S = 1.0`). +2. **Warning is orange, not yellow.** Yellow cannot stay yellow at a lightness + low enough to clear 4.5:1 on white — it *becomes* olive. That is + colorimetric, not a tuning problem. Orange holds its identity when darkened, + so warning is `#AD5100` in light and `#FF9F0A` in dark. + +When adding a colour, search for the most saturated value that passes, not the +darkest. The darkest is always easy and always wrong. | `AppTextSecondary` | `#5A5A5F` | `#A1A1A6` | 6.15 | 7.50 | `AppTextSecondary` replaces `.secondary` for body text. iOS's own `secondaryLabel` diff --git a/DomainDig/BatchResultsView.swift b/DomainDig/BatchResultsView.swift index bf211dc..4b4f6fd 100644 --- a/DomainDig/BatchResultsView.swift +++ b/DomainDig/BatchResultsView.swift @@ -104,13 +104,13 @@ struct BatchResultRowView: View { if let changeClassification = result.changeClassification { Text("Impact: \(changeClassification.title)") .font(appDensity.font(.caption2)) - .foregroundStyle(changeClassification.color) + .foregroundStyle(changeClassification.tone.foreground) } if let errorMessage = result.errorMessage { Text(errorMessage) .font(appDensity.font(.caption2)) - .foregroundStyle(result.status == .failed ? Color(.statusCritical) : .secondary) + .foregroundStyle(result.status == .failed ? Color(.statusCritical) : Color(.appTextSecondary)) } } .frame(maxWidth: .infinity, alignment: .leading) @@ -147,20 +147,20 @@ struct BatchResultRowView: View { case .pending: return .init(title: "Pending", systemImage: "clock", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) case .running: - return .init(title: "Running", systemImage: "arrow.clockwise", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16)) + return .init(title: "Running", systemImage: "arrow.clockwise", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfoSurface)) case .completed: if result.changeClassification == .critical || result.certificateWarningLevel == .critical || result.riskLevel == .high { - return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16)) + return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface)) } if result.changeClassification == .warning || result.changeSeverity == .medium || result.certificateWarningLevel == .warning { - return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) + return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) } if result.quickStatus == "Changed" { - return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16)) + return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfoSurface)) } - return .init(title: "Stable", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)) + return .init(title: "Stable", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) case .failed: - return .init(title: "Failed", systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16)) + return .init(title: "Failed", systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface)) } } } diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift index efaaf05..47512c8 100644 --- a/DomainDig/ContentView.swift +++ b/DomainDig/ContentView.swift @@ -1029,21 +1029,21 @@ struct DomainChangeSummaryView: View { HStack { Label(summary.hasChanges ? "Changed" : "Stable", systemImage: summary.hasChanges ? "arrow.triangle.2.circlepath" : "checkmark.circle") .font(appDensity.font(.caption)) - .foregroundStyle(summary.hasChanges ? severityColor(summary.severity) : Color(.statusPositive)) + .foregroundStyle(summary.hasChanges ? severityTone(summary.severity).foreground : Color(.statusPositive)) Spacer() Text(summary.severity.title.uppercased()) .font(appDensity.font(.caption2)) - .foregroundStyle(summary.hasChanges ? severityColor(summary.severity) : .secondary) + .foregroundStyle(summary.hasChanges ? severityTone(summary.severity).foreground : Color(.appTextSecondary)) .padding(.horizontal, 8) .padding(.vertical, 4) - .background((summary.hasChanges ? severityColor(summary.severity) : .secondary).opacity(0.16)) + .background((summary.hasChanges ? severityTone(summary.severity) : AppStatusTone.neutral).surface) .clipShape(Capsule()) Text(summary.impactClassification.title.uppercased()) .font(appDensity.font(.caption2)) - .foregroundStyle(summary.impactClassification.color) + .foregroundStyle(summary.impactClassification.tone.foreground) .padding(.horizontal, 8) .padding(.vertical, 4) - .background(summary.impactClassification.color.opacity(0.16)) + .background(summary.impactClassification.tone.surface) .clipShape(Capsule()) Text(summary.generatedAt, style: .time) .font(appDensity.font(.caption2)) @@ -1096,14 +1096,14 @@ struct DomainChangeSummaryView: View { } } - private func severityColor(_ severity: ChangeSeverity) -> Color { + private func severityTone(_ severity: ChangeSeverity) -> AppStatusTone { switch severity { case .low: - return .secondary + return .neutral case .medium: - return Color(.statusWarning) + return .warning case .high: - return Color(.statusCritical) + return .critical } } @@ -1173,10 +1173,10 @@ struct DomainDiffView: View { Spacer() Text("\(item.changeType.marker) \(item.severity.title) • \(changeLabel(for: item.changeType))") .font(.system(.caption2, design: .monospaced)) - .foregroundStyle(changeColor(for: item)) + .foregroundStyle(changeTone(for: item).foreground) .padding(.horizontal, 8) .padding(.vertical, 4) - .background(changeColor(for: item).opacity(0.16)) + .background(changeTone(for: item).surface) .clipShape(Capsule()) } @@ -1205,7 +1205,7 @@ struct DomainDiffView: View { } } .padding(10) - .background(item.hasChanges ? changeColor(for: item).opacity(0.08) : Color(.appSurface)) + .background(item.hasChanges ? changeTone(for: item).surface : Color(.appSurface)) .cornerRadius(8) } } label: { @@ -1249,18 +1249,18 @@ struct DomainDiffView: View { } } - private func changeColor(for item: DomainDiffItem) -> Color { + private func changeTone(for item: DomainDiffItem) -> AppStatusTone { if item.changeType == .unchanged { - return .secondary + return .neutral } switch item.severity { case .low: - return .blue + return .info case .medium: - return Color(.statusWarning) + return .warning case .high: - return Color(.statusCritical) + return .critical } } @@ -1337,7 +1337,7 @@ struct DomainSectionView: View { CollapsibleSectionView(title: "Domain", isCollapsed: $isCollapsed) { if let trackedDomain { HStack(spacing: 8) { - AppStatusBadgeView(model: .init(title: "Tracked", systemImage: "eye.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))) + AppStatusBadgeView(model: .init(title: "Tracked", systemImage: "eye.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))) Button { onTogglePinned() } label: { @@ -2186,9 +2186,9 @@ struct EmailSectionView: View { private func emailRowBadge(_ row: EmailRowViewData) -> AppStatusBadgeModel { switch row.statusTone { case .success: - return .init(title: row.status, systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)) + return .init(title: row.status, systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) case .warning: - return .init(title: row.status, systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) + return .init(title: row.status, systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) case .failure: return .init(title: row.status, systemImage: "minus.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) case .primary, .secondary: @@ -2345,11 +2345,11 @@ struct NetworkSectionView: View { private func reachabilityBadge(_ row: ReachabilityRowViewData) -> AppStatusBadgeModel { switch row.statusTone { case .success: - return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)) + return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) case .warning: - return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) + return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) case .failure: - return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16)) + return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface)) case .primary, .secondary: return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) } @@ -2396,11 +2396,11 @@ struct PortRowsView: View { private func portBadge(_ row: PortScanRowViewData) -> AppStatusBadgeModel { switch row.statusTone { case .success: - return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)) + return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) case .warning: - return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) + return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) case .failure: - return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16)) + return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface)) case .primary, .secondary: return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) } @@ -3754,11 +3754,11 @@ private struct DataImportPreviewSheet: View { } extension ChangeImpactClassification { - var color: Color { + var tone: AppStatusTone { switch self { - case .informational: return .secondary - case .warning: return Color(.statusWarning) - case .critical: return Color(.statusCritical) + case .informational: return .neutral + case .warning: return .warning + case .critical: return .critical } } } diff --git a/DomainDig/DashboardView.swift b/DomainDig/DashboardView.swift index 8d41ccb..16c4478 100644 --- a/DomainDig/DashboardView.swift +++ b/DomainDig/DashboardView.swift @@ -237,20 +237,20 @@ struct DashboardView: View { let criticalCount = states.filter { $0.health == .critical }.count let warningCount = states.filter { $0.health == .warning }.count let title: String - let color: Color + let tone: AppStatusTone let systemImage: String if criticalCount > 0 { title = "\(criticalCount) critical" - color = Color(.statusCritical) + tone = .critical systemImage = "exclamationmark.octagon.fill" } else if warningCount > 0 { title = "\(warningCount) warning" - color = Color(.statusWarning) + tone = .warning systemImage = "exclamationmark.triangle.fill" } else { title = "Healthy" - color = Color(.statusPositive) + tone = .positive systemImage = "checkmark.circle.fill" } @@ -258,8 +258,8 @@ struct DashboardView: View { model: .init( title: title, systemImage: systemImage, - foregroundColor: color, - backgroundColor: color.opacity(0.16) + foregroundColor: tone.foreground, + backgroundColor: tone.surface ) ) } @@ -331,11 +331,11 @@ private struct PortfolioAttentionRow: View { private var badgeModel: AppStatusBadgeModel { switch item.health { case .healthy: - return .init(title: "Healthy", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)) + return .init(title: "Healthy", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) case .warning: - return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) + return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) case .critical: - return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16)) + return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface)) } } } @@ -370,11 +370,11 @@ private struct PortfolioExpiryRow: View { private var badgeModel: AppStatusBadgeModel { switch state.certificateExpiryState { case .none: - return .init(title: "Healthy", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)) + return .init(title: "Healthy", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) case .warning: - return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) + return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) case .critical: - return .init(title: "Critical", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16)) + return .init(title: "Critical", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface)) } } } diff --git a/DomainDig/DomainDig/AuditViews.swift b/DomainDig/DomainDig/AuditViews.swift index 50ea137..4e57837 100644 --- a/DomainDig/DomainDig/AuditViews.swift +++ b/DomainDig/DomainDig/AuditViews.swift @@ -526,33 +526,33 @@ private func auditStatusBadge(_ status: AuditStatus) -> some View { let model: AppStatusBadgeModel switch status { case .draft: - model = .init(title: "Draft", systemImage: "square.and.pencil", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) + model = .init(title: "Draft", systemImage: "square.and.pencil", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) case .inReview: - model = .init(title: "In Review", systemImage: "doc.text.magnifyingglass", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16)) + model = .init(title: "In Review", systemImage: "doc.text.magnifyingglass", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfoSurface)) case .complete: - model = .init(title: "Complete", systemImage: "checkmark.seal.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)) + model = .init(title: "Complete", systemImage: "checkmark.seal.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) } return AppStatusBadgeView(model: model) } private func findingSeverityBadge(_ severity: AuditFindingSeverity) -> some View { - let color: Color + let tone: AppStatusTone switch severity { case .informational: - color = .secondary + tone = .neutral case .low: - color = Color(.statusPositive) + tone = .positive case .medium: - color = Color(.statusWarning) + tone = .warning case .high: - color = Color(.statusCritical) + tone = .critical } return AppStatusBadgeView( model: .init( title: severity.title, systemImage: "exclamationmark.circle.fill", - foregroundColor: color, - backgroundColor: color.opacity(0.16) + foregroundColor: tone.foreground, + backgroundColor: tone.surface ) ) } diff --git a/DomainDig/DomainDigUI.swift b/DomainDig/DomainDigUI.swift index 7d99e4e..3aec259 100644 --- a/DomainDig/DomainDigUI.swift +++ b/DomainDig/DomainDigUI.swift @@ -120,6 +120,53 @@ extension EnvironmentValues { } } +/// A status colour pairing: the foreground and the surface it sits on. +/// +/// These travel together because they cannot be derived from one another. The +/// badge fill used to be `foreground.opacity(0.16)`, which forced every +/// foreground dark enough to stay legible against its own wash — that is how the +/// light palette ended up olive-and-mud rather than amber-and-green. Decoupling +/// them lets the foregrounds stay fully saturated. +/// +/// See `Docs/ACCESSIBILITY.md` for the measured ratios. +enum AppStatusTone { + case positive + case warning + case critical + case info + case neutral + + var foreground: Color { + switch self { + case .positive: + return Color(.statusPositive) + case .warning: + return Color(.statusWarning) + case .critical: + return Color(.statusCritical) + case .info: + return Color(.statusInfo) + case .neutral: + return Color(.statusNeutral) + } + } + + var surface: Color { + switch self { + case .positive: + return Color(.statusPositiveSurface) + case .warning: + return Color(.statusWarningSurface) + case .critical: + return Color(.statusCriticalSurface) + case .info: + return Color(.statusInfoSurface) + case .neutral: + return Color(.statusNeutralSurface) + } + } +} + struct AppStatusBadgeModel: Equatable { let title: String let systemImage: String? @@ -131,9 +178,9 @@ enum AppStatusFactory { static func availability(_ status: DomainAvailabilityStatus?) -> AppStatusBadgeModel { switch status { case .available: - return .init(title: "Available", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)) + return .init(title: "Available", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) case .registered: - return .init(title: "Registered", systemImage: "circle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) + return .init(title: "Registered", systemImage: "circle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) case .unknown, .none: return .init(title: "Unknown", systemImage: "questionmark.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) } @@ -141,12 +188,12 @@ enum AppStatusFactory { static func tls(sslInfo: SSLCertificateInfo?, error: String?) -> AppStatusBadgeModel { if error != nil || sslInfo == nil { - return .init(title: "Invalid", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16)) + return .init(title: "Invalid", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface)) } if let sslInfo, sslInfo.daysUntilExpiry <= 14 { - return .init(title: "Expiring", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) + return .init(title: "Expiring", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) } - return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)) + return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) } static func email(_ result: EmailSecurityResult?, error: String?) -> AppStatusBadgeModel { @@ -157,9 +204,9 @@ enum AppStatusFactory { let foundCount = [result.spf.found, result.dmarc.found, result.dkim.found].filter { $0 }.count switch foundCount { case 3: - return .init(title: "Secure", systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)) + return .init(title: "Secure", systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) case 1, 2: - return .init(title: "Partial", systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) + return .init(title: "Partial", systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) default: return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) } @@ -170,7 +217,7 @@ enum AppStatusFactory { return .init(title: "Unchanged", systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) } if summary.hasChanges { - return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16)) + return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfoSurface)) } return .init(title: "Unchanged", systemImage: "checkmark.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) } diff --git a/DomainDig/HistoryView.swift b/DomainDig/HistoryView.swift index 5b788c3..f291c81 100644 --- a/DomainDig/HistoryView.swift +++ b/DomainDig/HistoryView.swift @@ -56,7 +56,7 @@ struct HistoryView: View { title: severity.title, systemImage: "arrow.triangle.2.circlepath", foregroundColor: severity == .high ? Color(.statusCritical) : Color(.statusWarning), - backgroundColor: (severity == .high ? Color(.statusCritical) : Color(.statusWarning)).opacity(0.16) + backgroundColor: (severity == .high ? AppStatusTone.critical : .warning).surface ) ) } diff --git a/DomainDig/MonitoringView.swift b/DomainDig/MonitoringView.swift index db6f685..3863e47 100644 --- a/DomainDig/MonitoringView.swift +++ b/DomainDig/MonitoringView.swift @@ -78,10 +78,10 @@ struct MonitoringView: View { HStack(spacing: 8) { metricBadge(title: "\(log.domainsChecked) checked") if log.changesFound > 0 { - metricBadge(title: "\(log.changesFound) changed", tint: Color(.statusWarning)) + metricBadge(title: "\(log.changesFound) changed", tone: .warning) } if log.alertsTriggered > 0 { - metricBadge(title: "\(log.alertsTriggered) alerts", tint: Color(.statusCritical)) + metricBadge(title: "\(log.alertsTriggered) alerts", tone: .critical) } } } @@ -100,13 +100,13 @@ struct MonitoringView: View { } } - private func metricBadge(title: String, tint: Color = Color(.statusInfo)) -> some View { + private func metricBadge(title: String, tone: AppStatusTone = .info) -> some View { Text(title) .font(appDensity.font(.caption2)) - .foregroundStyle(tint) + .foregroundStyle(tone.foreground) .padding(.horizontal, 8) .padding(.vertical, 4) - .background(tint.opacity(0.16)) + .background(tone.surface) .clipShape(Capsule()) } diff --git a/DomainDig/TimelineView.swift b/DomainDig/TimelineView.swift index 16e2bf8..5352a94 100644 --- a/DomainDig/TimelineView.swift +++ b/DomainDig/TimelineView.swift @@ -117,7 +117,7 @@ private struct TimelineRow: View { title: severity.title, systemImage: "arrow.triangle.2.circlepath", foregroundColor: severity == .high ? Color(.statusCritical) : Color(.statusWarning), - backgroundColor: (severity == .high ? Color(.statusCritical) : Color(.statusWarning)).opacity(0.16) + backgroundColor: (severity == .high ? AppStatusTone.critical : .warning).surface ) ) } diff --git a/DomainDig/WatchlistView.swift b/DomainDig/WatchlistView.swift index ddb6f4a..dda97ad 100644 --- a/DomainDig/WatchlistView.swift +++ b/DomainDig/WatchlistView.swift @@ -516,7 +516,7 @@ struct WatchlistRowView: View { title: "Shared", systemImage: "person.2.fill", foregroundColor: Color(.statusInfo), - backgroundColor: Color(.statusInfo).opacity(0.16) + backgroundColor: Color(.statusInfoSurface) ) ) } @@ -533,11 +533,11 @@ struct WatchlistRowView: View { let days = trackedDomain.certificateDaysRemaining.map { "\($0)d" } ?? "Soon" switch trackedDomain.certificateWarningLevel { case .critical: - return .init(title: "Invalid \(days)", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16)) + return .init(title: "Invalid \(days)", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface)) case .warning: - return .init(title: "Expiring \(days)", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) + return .init(title: "Expiring \(days)", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface)) case .none: - return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)) + return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)) } } } diff --git a/Shared/Colors.xcassets/StatusCritical.colorset/Contents.json b/Shared/Colors.xcassets/StatusCritical.colorset/Contents.json index 8f97cd9..2963513 100644 --- a/Shared/Colors.xcassets/StatusCritical.colorset/Contents.json +++ b/Shared/Colors.xcassets/StatusCritical.colorset/Contents.json @@ -5,9 +5,9 @@ "color": { "color-space": "srgb", "components": { - "red": "0xB3", - "green": "0x26", - "blue": "0x1E", + "red": "0xCC", + "green": "0x07", + "blue": "0x00", "alpha": "1.000" } } @@ -41,9 +41,9 @@ "color": { "color-space": "srgb", "components": { - "red": "0x8C", - "green": "0x1D", - "blue": "0x16", + "red": "0xA3", + "green": "0x05", + "blue": "0x00", "alpha": "1.000" } } diff --git a/Shared/Colors.xcassets/StatusCriticalSurface.colorset/Contents.json b/Shared/Colors.xcassets/StatusCriticalSurface.colorset/Contents.json new file mode 100644 index 0000000..31b3941 --- /dev/null +++ b/Shared/Colors.xcassets/StatusCriticalSurface.colorset/Contents.json @@ -0,0 +1,78 @@ +{ + "colors": [ + { + "idiom": "universal", + "color": { + "color-space": "srgb", + "components": { + "red": "0xFB", + "green": "0xE0", + "blue": "0xDF", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0x2E", + "green": "0x13", + "blue": "0x10", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "contrast", + "value": "high" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0xF7", + "green": "0xCF", + "blue": "0xCD", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + }, + { + "appearance": "contrast", + "value": "high" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0x3D", + "green": "0x19", + "blue": "0x15", + "alpha": "1.000" + } + } + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Shared/Colors.xcassets/StatusInfoSurface.colorset/Contents.json b/Shared/Colors.xcassets/StatusInfoSurface.colorset/Contents.json new file mode 100644 index 0000000..e0a2c53 --- /dev/null +++ b/Shared/Colors.xcassets/StatusInfoSurface.colorset/Contents.json @@ -0,0 +1,78 @@ +{ + "colors": [ + { + "idiom": "universal", + "color": { + "color-space": "srgb", + "components": { + "red": "0xDF", + "green": "0xE3", + "blue": "0xFB", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0x10", + "green": "0x1C", + "blue": "0x33", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "contrast", + "value": "high" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0xCB", + "green": "0xD2", + "blue": "0xF7", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + }, + { + "appearance": "contrast", + "value": "high" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0x15", + "green": "0x27", + "blue": "0x44", + "alpha": "1.000" + } + } + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Shared/Colors.xcassets/StatusNeutralSurface.colorset/Contents.json b/Shared/Colors.xcassets/StatusNeutralSurface.colorset/Contents.json new file mode 100644 index 0000000..8b8c6bd --- /dev/null +++ b/Shared/Colors.xcassets/StatusNeutralSurface.colorset/Contents.json @@ -0,0 +1,78 @@ +{ + "colors": [ + { + "idiom": "universal", + "color": { + "color-space": "srgb", + "components": { + "red": "0xEB", + "green": "0xED", + "blue": "0xEF", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0x1A", + "green": "0x1A", + "blue": "0x1C", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "contrast", + "value": "high" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0xDF", + "green": "0xE1", + "blue": "0xE4", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + }, + { + "appearance": "contrast", + "value": "high" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0x23", + "green": "0x23", + "blue": "0x26", + "alpha": "1.000" + } + } + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Shared/Colors.xcassets/StatusPositive.colorset/Contents.json b/Shared/Colors.xcassets/StatusPositive.colorset/Contents.json index 22ce5e5..30280d9 100644 --- a/Shared/Colors.xcassets/StatusPositive.colorset/Contents.json +++ b/Shared/Colors.xcassets/StatusPositive.colorset/Contents.json @@ -5,9 +5,9 @@ "color": { "color-space": "srgb", "components": { - "red": "0x14", - "green": "0x6C", - "blue": "0x2E", + "red": "0x00", + "green": "0x80", + "blue": "0x35", "alpha": "1.000" } } @@ -41,9 +41,9 @@ "color": { "color-space": "srgb", "components": { - "red": "0x0B", - "green": "0x50", - "blue": "0x23", + "red": "0x00", + "green": "0x66", + "blue": "0x1F", "alpha": "1.000" } } diff --git a/Shared/Colors.xcassets/StatusPositiveSurface.colorset/Contents.json b/Shared/Colors.xcassets/StatusPositiveSurface.colorset/Contents.json new file mode 100644 index 0000000..dc80f8f --- /dev/null +++ b/Shared/Colors.xcassets/StatusPositiveSurface.colorset/Contents.json @@ -0,0 +1,78 @@ +{ + "colors": [ + { + "idiom": "universal", + "color": { + "color-space": "srgb", + "components": { + "red": "0xE3", + "green": "0xF7", + "blue": "0xEC", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0x0E", + "green": "0x2A", + "blue": "0x18", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "contrast", + "value": "high" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0xD3", + "green": "0xF0", + "blue": "0xE0", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + }, + { + "appearance": "contrast", + "value": "high" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0x12", + "green": "0x3A", + "blue": "0x20", + "alpha": "1.000" + } + } + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Shared/Colors.xcassets/StatusWarning.colorset/Contents.json b/Shared/Colors.xcassets/StatusWarning.colorset/Contents.json index cc79e86..003f80c 100644 --- a/Shared/Colors.xcassets/StatusWarning.colorset/Contents.json +++ b/Shared/Colors.xcassets/StatusWarning.colorset/Contents.json @@ -5,8 +5,8 @@ "color": { "color-space": "srgb", "components": { - "red": "0x7A", - "green": "0x56", + "red": "0xAD", + "green": "0x51", "blue": "0x00", "alpha": "1.000" } @@ -24,7 +24,7 @@ "color-space": "srgb", "components": { "red": "0xFF", - "green": "0xD6", + "green": "0x9F", "blue": "0x0A", "alpha": "1.000" } @@ -41,8 +41,8 @@ "color": { "color-space": "srgb", "components": { - "red": "0x5C", - "green": "0x41", + "red": "0x8A", + "green": "0x3F", "blue": "0x00", "alpha": "1.000" } @@ -64,8 +64,8 @@ "color-space": "srgb", "components": { "red": "0xFF", - "green": "0xE5", - "blue": "0x66", + "green": "0xBF", + "blue": "0x5C", "alpha": "1.000" } } diff --git a/Shared/Colors.xcassets/StatusWarningSurface.colorset/Contents.json b/Shared/Colors.xcassets/StatusWarningSurface.colorset/Contents.json new file mode 100644 index 0000000..8030a19 --- /dev/null +++ b/Shared/Colors.xcassets/StatusWarningSurface.colorset/Contents.json @@ -0,0 +1,78 @@ +{ + "colors": [ + { + "idiom": "universal", + "color": { + "color-space": "srgb", + "components": { + "red": "0xFB", + "green": "0xEC", + "blue": "0xDF", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0x2E", + "green": "0x1F", + "blue": "0x08", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "contrast", + "value": "high" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0xF7", + "green": "0xE0", + "blue": "0xC9", + "alpha": "1.000" + } + } + }, + { + "idiom": "universal", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + }, + { + "appearance": "contrast", + "value": "high" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0x3D", + "green": "0x2A", + "blue": "0x0B", + "alpha": "1.000" + } + } + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} -- cgit v1.2.3