Commit 9e58a36915

9e58a36915bb1fe8cf88423c9519c0c12dbbf2e2

parent: 2c608ed9cb

Unsigned

cmc <hello@cleberg.net> · 2026-07-21 01:25 UTC

fix(a11y): rebalance the light palette so hues survive

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.

Layout: unified · split

Docs/ACCESSIBILITY.md +31 −5
@@ -24,11 +24,37 @@ worst of those three is shown:
2424
2525| Role | Light | Dark | Worst light | Worst dark |
2626| --- | --- | --- | --- | --- |
27| `StatusInfo` / `AccentColor` | `#0000FF` | `#4DA3FF` | 5.50 | 5.85 |
28| `StatusPositive` | `#146C2E` | `#30D158` | 4.65 | 7.32 |
29| `StatusWarning` | `#7A5600` | `#FFD60A` | 4.74 | 9.61 |
30| `StatusCritical` | `#B3261E` | `#FF6961` | 4.51 | 5.54 |
31| `StatusNeutral` | `#5A5A5F` | `#A1A1A6` | 4.92 | 5.88 |
27| `StatusInfo` / `AccentColor` | `#0000FF` | `#4DA3FF` | 6.76 | 6.47 |
28| `StatusPositive` | `#008035` | `#30D158` | 4.54 | 7.62 |
29| `StatusWarning` | `#AD5100` | `#FF9F0A` | 4.59 | 7.76 |
30| `StatusCritical` | `#CC0700` | `#FF6961` | 4.68 | 6.12 |
31| `StatusNeutral` | `#5A5A5F` | `#A1A1A6` | 5.84 | 6.76 |
32
33Each status foreground has a matching `…Surface` colour for the fill behind it,
34paired through `AppStatusTone`.
35
36### Contrast alone is not a palette
37
38The first version of this palette maximised contrast and produced mud. Requiring
39every foreground to clear 4.5:1 against *its own 16% tint* — the harshest
40surface it ever sits on — pushed each colour ~20% darker than the common case
41needed. `#7A5600` is not amber, it is olive; `#146C2E` is not green so much as
42bottle-dark. Contrast passed and the UI was still hard to read, because hue
43identity is what tells "warning" from "critical" at a glance.
44
45Two fixes:
46
471. **Decouple the fill from the foreground.** `AppStatusTone` carries a
48 `foreground` and a `surface` that are authored independently, so the
49 foreground no longer has to survive a wash of itself. Every status foreground
50 is now fully saturated (`S = 1.0`).
512. **Warning is orange, not yellow.** Yellow cannot stay yellow at a lightness
52 low enough to clear 4.5:1 on white — it *becomes* olive. That is
53 colorimetric, not a tuning problem. Orange holds its identity when darkened,
54 so warning is `#AD5100` in light and `#FF9F0A` in dark.
55
56When adding a colour, search for the most saturated value that passes, not the
57darkest. The darkest is always easy and always wrong.
3258| `AppTextSecondary` | `#5A5A5F` | `#A1A1A6` | 6.15 | 7.50 |
3359
3460`AppTextSecondary` replaces `.secondary` for body text. iOS's own `secondaryLabel`
DomainDig/BatchResultsView.swift +8 −8
@@ -104,13 +104,13 @@ struct BatchResultRowView: View {
104104 if let changeClassification = result.changeClassification {
105105 Text("Impact: \(changeClassification.title)")
106106 .font(appDensity.font(.caption2))
107 .foregroundStyle(changeClassification.color)
107 .foregroundStyle(changeClassification.tone.foreground)
108108 }
109109
110110 if let errorMessage = result.errorMessage {
111111 Text(errorMessage)
112112 .font(appDensity.font(.caption2))
113 .foregroundStyle(result.status == .failed ? Color(.statusCritical) : .secondary)
113 .foregroundStyle(result.status == .failed ? Color(.statusCritical) : Color(.appTextSecondary))
114114 }
115115 }
116116 .frame(maxWidth: .infinity, alignment: .leading)
@@ -147,20 +147,20 @@ struct BatchResultRowView: View {
147147 case .pending:
148148 return .init(title: "Pending", systemImage: "clock", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
149149 case .running:
150 return .init(title: "Running", systemImage: "arrow.clockwise", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16))
150 return .init(title: "Running", systemImage: "arrow.clockwise", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfoSurface))
151151 case .completed:
152152 if result.changeClassification == .critical || result.certificateWarningLevel == .critical || result.riskLevel == .high {
153 return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
153 return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
154154 }
155155 if result.changeClassification == .warning || result.changeSeverity == .medium || result.certificateWarningLevel == .warning {
156 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
156 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
157157 }
158158 if result.quickStatus == "Changed" {
159 return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16))
159 return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfoSurface))
160160 }
161 return .init(title: "Stable", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
161 return .init(title: "Stable", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
162162 case .failed:
163 return .init(title: "Failed", systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
163 return .init(title: "Failed", systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
164164 }
165165 }
166166}
DomainDig/ContentView.swift +30 −30
@@ -1029,21 +1029,21 @@ struct DomainChangeSummaryView: View {
10291029 HStack {
10301030 Label(summary.hasChanges ? "Changed" : "Stable", systemImage: summary.hasChanges ? "arrow.triangle.2.circlepath" : "checkmark.circle")
10311031 .font(appDensity.font(.caption))
1032 .foregroundStyle(summary.hasChanges ? severityColor(summary.severity) : Color(.statusPositive))
1032 .foregroundStyle(summary.hasChanges ? severityTone(summary.severity).foreground : Color(.statusPositive))
10331033 Spacer()
10341034 Text(summary.severity.title.uppercased())
10351035 .font(appDensity.font(.caption2))
1036 .foregroundStyle(summary.hasChanges ? severityColor(summary.severity) : .secondary)
1036 .foregroundStyle(summary.hasChanges ? severityTone(summary.severity).foreground : Color(.appTextSecondary))
10371037 .padding(.horizontal, 8)
10381038 .padding(.vertical, 4)
1039 .background((summary.hasChanges ? severityColor(summary.severity) : .secondary).opacity(0.16))
1039 .background((summary.hasChanges ? severityTone(summary.severity) : AppStatusTone.neutral).surface)
10401040 .clipShape(Capsule())
10411041 Text(summary.impactClassification.title.uppercased())
10421042 .font(appDensity.font(.caption2))
1043 .foregroundStyle(summary.impactClassification.color)
1043 .foregroundStyle(summary.impactClassification.tone.foreground)
10441044 .padding(.horizontal, 8)
10451045 .padding(.vertical, 4)
1046 .background(summary.impactClassification.color.opacity(0.16))
1046 .background(summary.impactClassification.tone.surface)
10471047 .clipShape(Capsule())
10481048 Text(summary.generatedAt, style: .time)
10491049 .font(appDensity.font(.caption2))
@@ -1096,14 +1096,14 @@ struct DomainChangeSummaryView: View {
10961096 }
10971097 }
10981098
1099 private func severityColor(_ severity: ChangeSeverity) -> Color {
1099 private func severityTone(_ severity: ChangeSeverity) -> AppStatusTone {
11001100 switch severity {
11011101 case .low:
1102 return .secondary
1102 return .neutral
11031103 case .medium:
1104 return Color(.statusWarning)
1104 return .warning
11051105 case .high:
1106 return Color(.statusCritical)
1106 return .critical
11071107 }
11081108 }
11091109
@@ -1173,10 +1173,10 @@ struct DomainDiffView: View {
11731173 Spacer()
11741174 Text("\(item.changeType.marker) \(item.severity.title) • \(changeLabel(for: item.changeType))")
11751175 .font(.system(.caption2, design: .monospaced))
1176 .foregroundStyle(changeColor(for: item))
1176 .foregroundStyle(changeTone(for: item).foreground)
11771177 .padding(.horizontal, 8)
11781178 .padding(.vertical, 4)
1179 .background(changeColor(for: item).opacity(0.16))
1179 .background(changeTone(for: item).surface)
11801180 .clipShape(Capsule())
11811181 }
11821182
@@ -1205,7 +1205,7 @@ struct DomainDiffView: View {
12051205 }
12061206 }
12071207 .padding(10)
1208 .background(item.hasChanges ? changeColor(for: item).opacity(0.08) : Color(.appSurface))
1208 .background(item.hasChanges ? changeTone(for: item).surface : Color(.appSurface))
12091209 .cornerRadius(8)
12101210 }
12111211 } label: {
@@ -1249,18 +1249,18 @@ struct DomainDiffView: View {
12491249 }
12501250 }
12511251
1252 private func changeColor(for item: DomainDiffItem) -> Color {
1252 private func changeTone(for item: DomainDiffItem) -> AppStatusTone {
12531253 if item.changeType == .unchanged {
1254 return .secondary
1254 return .neutral
12551255 }
12561256
12571257 switch item.severity {
12581258 case .low:
1259 return .blue
1259 return .info
12601260 case .medium:
1261 return Color(.statusWarning)
1261 return .warning
12621262 case .high:
1263 return Color(.statusCritical)
1263 return .critical
12641264 }
12651265 }
12661266
@@ -1337,7 +1337,7 @@ struct DomainSectionView: View {
13371337 CollapsibleSectionView(title: "Domain", isCollapsed: $isCollapsed) {
13381338 if let trackedDomain {
13391339 HStack(spacing: 8) {
1340 AppStatusBadgeView(model: .init(title: "Tracked", systemImage: "eye.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)))
1340 AppStatusBadgeView(model: .init(title: "Tracked", systemImage: "eye.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)))
13411341 Button {
13421342 onTogglePinned()
13431343 } label: {
@@ -2186,9 +2186,9 @@ struct EmailSectionView: View {
21862186 private func emailRowBadge(_ row: EmailRowViewData) -> AppStatusBadgeModel {
21872187 switch row.statusTone {
21882188 case .success:
2189 return .init(title: row.status, systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
2189 return .init(title: row.status, systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
21902190 case .warning:
2191 return .init(title: row.status, systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
2191 return .init(title: row.status, systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
21922192 case .failure:
21932193 return .init(title: row.status, systemImage: "minus.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
21942194 case .primary, .secondary:
@@ -2345,11 +2345,11 @@ struct NetworkSectionView: View {
23452345 private func reachabilityBadge(_ row: ReachabilityRowViewData) -> AppStatusBadgeModel {
23462346 switch row.statusTone {
23472347 case .success:
2348 return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
2348 return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
23492349 case .warning:
2350 return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
2350 return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
23512351 case .failure:
2352 return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
2352 return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
23532353 case .primary, .secondary:
23542354 return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
23552355 }
@@ -2396,11 +2396,11 @@ struct PortRowsView: View {
23962396 private func portBadge(_ row: PortScanRowViewData) -> AppStatusBadgeModel {
23972397 switch row.statusTone {
23982398 case .success:
2399 return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
2399 return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
24002400 case .warning:
2401 return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
2401 return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
24022402 case .failure:
2403 return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
2403 return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
24042404 case .primary, .secondary:
24052405 return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
24062406 }
@@ -3754,11 +3754,11 @@ private struct DataImportPreviewSheet: View {
37543754}
37553755
37563756extension ChangeImpactClassification {
3757 var color: Color {
3757 var tone: AppStatusTone {
37583758 switch self {
3759 case .informational: return .secondary
3760 case .warning: return Color(.statusWarning)
3761 case .critical: return Color(.statusCritical)
3759 case .informational: return .neutral
3760 case .warning: return .warning
3761 case .critical: return .critical
37623762 }
37633763 }
37643764}
DomainDig/DashboardView.swift +12 −12
@@ -237,20 +237,20 @@ struct DashboardView: View {
237237 let criticalCount = states.filter { $0.health == .critical }.count
238238 let warningCount = states.filter { $0.health == .warning }.count
239239 let title: String
240 let color: Color
240 let tone: AppStatusTone
241241 let systemImage: String
242242
243243 if criticalCount > 0 {
244244 title = "\(criticalCount) critical"
245 color = Color(.statusCritical)
245 tone = .critical
246246 systemImage = "exclamationmark.octagon.fill"
247247 } else if warningCount > 0 {
248248 title = "\(warningCount) warning"
249 color = Color(.statusWarning)
249 tone = .warning
250250 systemImage = "exclamationmark.triangle.fill"
251251 } else {
252252 title = "Healthy"
253 color = Color(.statusPositive)
253 tone = .positive
254254 systemImage = "checkmark.circle.fill"
255255 }
256256
@@ -258,8 +258,8 @@ struct DashboardView: View {
258258 model: .init(
259259 title: title,
260260 systemImage: systemImage,
261 foregroundColor: color,
262 backgroundColor: color.opacity(0.16)
261 foregroundColor: tone.foreground,
262 backgroundColor: tone.surface
263263 )
264264 )
265265 }
@@ -331,11 +331,11 @@ private struct PortfolioAttentionRow: View {
331331 private var badgeModel: AppStatusBadgeModel {
332332 switch item.health {
333333 case .healthy:
334 return .init(title: "Healthy", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
334 return .init(title: "Healthy", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
335335 case .warning:
336 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
336 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
337337 case .critical:
338 return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
338 return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
339339 }
340340 }
341341}
@@ -370,11 +370,11 @@ private struct PortfolioExpiryRow: View {
370370 private var badgeModel: AppStatusBadgeModel {
371371 switch state.certificateExpiryState {
372372 case .none:
373 return .init(title: "Healthy", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
373 return .init(title: "Healthy", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
374374 case .warning:
375 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
375 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
376376 case .critical:
377 return .init(title: "Critical", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
377 return .init(title: "Critical", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
378378 }
379379 }
380380}
DomainDig/DomainDig/AuditViews.swift +10 −10
@@ -526,33 +526,33 @@ private func auditStatusBadge(_ status: AuditStatus) -> some View {
526526 let model: AppStatusBadgeModel
527527 switch status {
528528 case .draft:
529 model = .init(title: "Draft", systemImage: "square.and.pencil", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
529 model = .init(title: "Draft", systemImage: "square.and.pencil", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
530530 case .inReview:
531 model = .init(title: "In Review", systemImage: "doc.text.magnifyingglass", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16))
531 model = .init(title: "In Review", systemImage: "doc.text.magnifyingglass", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfoSurface))
532532 case .complete:
533 model = .init(title: "Complete", systemImage: "checkmark.seal.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
533 model = .init(title: "Complete", systemImage: "checkmark.seal.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
534534 }
535535 return AppStatusBadgeView(model: model)
536536}
537537
538538private func findingSeverityBadge(_ severity: AuditFindingSeverity) -> some View {
539 let color: Color
539 let tone: AppStatusTone
540540 switch severity {
541541 case .informational:
542 color = .secondary
542 tone = .neutral
543543 case .low:
544 color = Color(.statusPositive)
544 tone = .positive
545545 case .medium:
546 color = Color(.statusWarning)
546 tone = .warning
547547 case .high:
548 color = Color(.statusCritical)
548 tone = .critical
549549 }
550550 return AppStatusBadgeView(
551551 model: .init(
552552 title: severity.title,
553553 systemImage: "exclamationmark.circle.fill",
554 foregroundColor: color,
555 backgroundColor: color.opacity(0.16)
554 foregroundColor: tone.foreground,
555 backgroundColor: tone.surface
556556 )
557557 )
558558}
DomainDig/DomainDigUI.swift +55 −8
@@ -120,6 +120,53 @@ extension EnvironmentValues {
120120 }
121121}
122122
123/// A status colour pairing: the foreground and the surface it sits on.
124///
125/// These travel together because they cannot be derived from one another. The
126/// badge fill used to be `foreground.opacity(0.16)`, which forced every
127/// foreground dark enough to stay legible against its own wash — that is how the
128/// light palette ended up olive-and-mud rather than amber-and-green. Decoupling
129/// them lets the foregrounds stay fully saturated.
130///
131/// See `Docs/ACCESSIBILITY.md` for the measured ratios.
132enum AppStatusTone {
133 case positive
134 case warning
135 case critical
136 case info
137 case neutral
138
139 var foreground: Color {
140 switch self {
141 case .positive:
142 return Color(.statusPositive)
143 case .warning:
144 return Color(.statusWarning)
145 case .critical:
146 return Color(.statusCritical)
147 case .info:
148 return Color(.statusInfo)
149 case .neutral:
150 return Color(.statusNeutral)
151 }
152 }
153
154 var surface: Color {
155 switch self {
156 case .positive:
157 return Color(.statusPositiveSurface)
158 case .warning:
159 return Color(.statusWarningSurface)
160 case .critical:
161 return Color(.statusCriticalSurface)
162 case .info:
163 return Color(.statusInfoSurface)
164 case .neutral:
165 return Color(.statusNeutralSurface)
166 }
167 }
168}
169
123170struct AppStatusBadgeModel: Equatable {
124171 let title: String
125172 let systemImage: String?
@@ -131,9 +178,9 @@ enum AppStatusFactory {
131178 static func availability(_ status: DomainAvailabilityStatus?) -> AppStatusBadgeModel {
132179 switch status {
133180 case .available:
134 return .init(title: "Available", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
181 return .init(title: "Available", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
135182 case .registered:
136 return .init(title: "Registered", systemImage: "circle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
183 return .init(title: "Registered", systemImage: "circle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
137184 case .unknown, .none:
138185 return .init(title: "Unknown", systemImage: "questionmark.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
139186 }
@@ -141,12 +188,12 @@ enum AppStatusFactory {
141188
142189 static func tls(sslInfo: SSLCertificateInfo?, error: String?) -> AppStatusBadgeModel {
143190 if error != nil || sslInfo == nil {
144 return .init(title: "Invalid", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
191 return .init(title: "Invalid", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
145192 }
146193 if let sslInfo, sslInfo.daysUntilExpiry <= 14 {
147 return .init(title: "Expiring", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
194 return .init(title: "Expiring", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
148195 }
149 return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
196 return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
150197 }
151198
152199 static func email(_ result: EmailSecurityResult?, error: String?) -> AppStatusBadgeModel {
@@ -157,9 +204,9 @@ enum AppStatusFactory {
157204 let foundCount = [result.spf.found, result.dmarc.found, result.dkim.found].filter { $0 }.count
158205 switch foundCount {
159206 case 3:
160 return .init(title: "Secure", systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
207 return .init(title: "Secure", systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
161208 case 1, 2:
162 return .init(title: "Partial", systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
209 return .init(title: "Partial", systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
163210 default:
164211 return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
165212 }
@@ -170,7 +217,7 @@ enum AppStatusFactory {
170217 return .init(title: "Unchanged", systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
171218 }
172219 if summary.hasChanges {
173 return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16))
220 return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfoSurface))
174221 }
175222 return .init(title: "Unchanged", systemImage: "checkmark.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
176223 }
DomainDig/HistoryView.swift +1 −1
@@ -56,7 +56,7 @@ struct HistoryView: View {
5656 title: severity.title,
5757 systemImage: "arrow.triangle.2.circlepath",
5858 foregroundColor: severity == .high ? Color(.statusCritical) : Color(.statusWarning),
59 backgroundColor: (severity == .high ? Color(.statusCritical) : Color(.statusWarning)).opacity(0.16)
59 backgroundColor: (severity == .high ? AppStatusTone.critical : .warning).surface
6060 )
6161 )
6262 }
DomainDig/MonitoringView.swift +5 −5
@@ -78,10 +78,10 @@ struct MonitoringView: View {
7878 HStack(spacing: 8) {
7979 metricBadge(title: "\(log.domainsChecked) checked")
8080 if log.changesFound > 0 {
81 metricBadge(title: "\(log.changesFound) changed", tint: Color(.statusWarning))
81 metricBadge(title: "\(log.changesFound) changed", tone: .warning)
8282 }
8383 if log.alertsTriggered > 0 {
84 metricBadge(title: "\(log.alertsTriggered) alerts", tint: Color(.statusCritical))
84 metricBadge(title: "\(log.alertsTriggered) alerts", tone: .critical)
8585 }
8686 }
8787 }
@@ -100,13 +100,13 @@ struct MonitoringView: View {
100100 }
101101 }
102102
103 private func metricBadge(title: String, tint: Color = Color(.statusInfo)) -> some View {
103 private func metricBadge(title: String, tone: AppStatusTone = .info) -> some View {
104104 Text(title)
105105 .font(appDensity.font(.caption2))
106 .foregroundStyle(tint)
106 .foregroundStyle(tone.foreground)
107107 .padding(.horizontal, 8)
108108 .padding(.vertical, 4)
109 .background(tint.opacity(0.16))
109 .background(tone.surface)
110110 .clipShape(Capsule())
111111 }
112112
DomainDig/TimelineView.swift +1 −1
@@ -117,7 +117,7 @@ private struct TimelineRow: View {
117117 title: severity.title,
118118 systemImage: "arrow.triangle.2.circlepath",
119119 foregroundColor: severity == .high ? Color(.statusCritical) : Color(.statusWarning),
120 backgroundColor: (severity == .high ? Color(.statusCritical) : Color(.statusWarning)).opacity(0.16)
120 backgroundColor: (severity == .high ? AppStatusTone.critical : .warning).surface
121121 )
122122 )
123123 }
DomainDig/WatchlistView.swift +4 −4
@@ -516,7 +516,7 @@ struct WatchlistRowView: View {
516516 title: "Shared",
517517 systemImage: "person.2.fill",
518518 foregroundColor: Color(.statusInfo),
519 backgroundColor: Color(.statusInfo).opacity(0.16)
519 backgroundColor: Color(.statusInfoSurface)
520520 )
521521 )
522522 }
@@ -533,11 +533,11 @@ struct WatchlistRowView: View {
533533 let days = trackedDomain.certificateDaysRemaining.map { "\($0)d" } ?? "Soon"
534534 switch trackedDomain.certificateWarningLevel {
535535 case .critical:
536 return .init(title: "Invalid \(days)", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
536 return .init(title: "Invalid \(days)", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
537537 case .warning:
538 return .init(title: "Expiring \(days)", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
538 return .init(title: "Expiring \(days)", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
539539 case .none:
540 return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
540 return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
541541 }
542542 }
543543}
Shared/Colors.xcassets/StatusCritical.colorset/Contents.json +6 −6
@@ -5,9 +5,9 @@
55 "color": {
66 "color-space": "srgb",
77 "components": {
8 "red": "0xB3",
9 "green": "0x26",
10 "blue": "0x1E",
8 "red": "0xCC",
9 "green": "0x07",
10 "blue": "0x00",
1111 "alpha": "1.000"
1212 }
1313 }
@@ -41,9 +41,9 @@
4141 "color": {
4242 "color-space": "srgb",
4343 "components": {
44 "red": "0x8C",
45 "green": "0x1D",
46 "blue": "0x16",
44 "red": "0xA3",
45 "green": "0x05",
46 "blue": "0x00",
4747 "alpha": "1.000"
4848 }
4949 }
Shared/Colors.xcassets/StatusCriticalSurface.colorset/Contents.json added +78
@@ -0,0 +1,78 @@
1{
2 "colors": [
3 {
4 "idiom": "universal",
5 "color": {
6 "color-space": "srgb",
7 "components": {
8 "red": "0xFB",
9 "green": "0xE0",
10 "blue": "0xDF",
11 "alpha": "1.000"
12 }
13 }
14 },
15 {
16 "idiom": "universal",
17 "appearances": [
18 {
19 "appearance": "luminosity",
20 "value": "dark"
21 }
22 ],
23 "color": {
24 "color-space": "srgb",
25 "components": {
26 "red": "0x2E",
27 "green": "0x13",
28 "blue": "0x10",
29 "alpha": "1.000"
30 }
31 }
32 },
33 {
34 "idiom": "universal",
35 "appearances": [
36 {
37 "appearance": "contrast",
38 "value": "high"
39 }
40 ],
41 "color": {
42 "color-space": "srgb",
43 "components": {
44 "red": "0xF7",
45 "green": "0xCF",
46 "blue": "0xCD",
47 "alpha": "1.000"
48 }
49 }
50 },
51 {
52 "idiom": "universal",
53 "appearances": [
54 {
55 "appearance": "luminosity",
56 "value": "dark"
57 },
58 {
59 "appearance": "contrast",
60 "value": "high"
61 }
62 ],
63 "color": {
64 "color-space": "srgb",
65 "components": {
66 "red": "0x3D",
67 "green": "0x19",
68 "blue": "0x15",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}
Shared/Colors.xcassets/StatusInfoSurface.colorset/Contents.json added +78
@@ -0,0 +1,78 @@
1{
2 "colors": [
3 {
4 "idiom": "universal",
5 "color": {
6 "color-space": "srgb",
7 "components": {
8 "red": "0xDF",
9 "green": "0xE3",
10 "blue": "0xFB",
11 "alpha": "1.000"
12 }
13 }
14 },
15 {
16 "idiom": "universal",
17 "appearances": [
18 {
19 "appearance": "luminosity",
20 "value": "dark"
21 }
22 ],
23 "color": {
24 "color-space": "srgb",
25 "components": {
26 "red": "0x10",
27 "green": "0x1C",
28 "blue": "0x33",
29 "alpha": "1.000"
30 }
31 }
32 },
33 {
34 "idiom": "universal",
35 "appearances": [
36 {
37 "appearance": "contrast",
38 "value": "high"
39 }
40 ],
41 "color": {
42 "color-space": "srgb",
43 "components": {
44 "red": "0xCB",
45 "green": "0xD2",
46 "blue": "0xF7",
47 "alpha": "1.000"
48 }
49 }
50 },
51 {
52 "idiom": "universal",
53 "appearances": [
54 {
55 "appearance": "luminosity",
56 "value": "dark"
57 },
58 {
59 "appearance": "contrast",
60 "value": "high"
61 }
62 ],
63 "color": {
64 "color-space": "srgb",
65 "components": {
66 "red": "0x15",
67 "green": "0x27",
68 "blue": "0x44",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}
Shared/Colors.xcassets/StatusNeutralSurface.colorset/Contents.json added +78
@@ -0,0 +1,78 @@
1{
2 "colors": [
3 {
4 "idiom": "universal",
5 "color": {
6 "color-space": "srgb",
7 "components": {
8 "red": "0xEB",
9 "green": "0xED",
10 "blue": "0xEF",
11 "alpha": "1.000"
12 }
13 }
14 },
15 {
16 "idiom": "universal",
17 "appearances": [
18 {
19 "appearance": "luminosity",
20 "value": "dark"
21 }
22 ],
23 "color": {
24 "color-space": "srgb",
25 "components": {
26 "red": "0x1A",
27 "green": "0x1A",
28 "blue": "0x1C",
29 "alpha": "1.000"
30 }
31 }
32 },
33 {
34 "idiom": "universal",
35 "appearances": [
36 {
37 "appearance": "contrast",
38 "value": "high"
39 }
40 ],
41 "color": {
42 "color-space": "srgb",
43 "components": {
44 "red": "0xDF",
45 "green": "0xE1",
46 "blue": "0xE4",
47 "alpha": "1.000"
48 }
49 }
50 },
51 {
52 "idiom": "universal",
53 "appearances": [
54 {
55 "appearance": "luminosity",
56 "value": "dark"
57 },
58 {
59 "appearance": "contrast",
60 "value": "high"
61 }
62 ],
63 "color": {
64 "color-space": "srgb",
65 "components": {
66 "red": "0x23",
67 "green": "0x23",
68 "blue": "0x26",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}
Shared/Colors.xcassets/StatusPositive.colorset/Contents.json +6 −6
@@ -5,9 +5,9 @@
55 "color": {
66 "color-space": "srgb",
77 "components": {
8 "red": "0x14",
9 "green": "0x6C",
10 "blue": "0x2E",
8 "red": "0x00",
9 "green": "0x80",
10 "blue": "0x35",
1111 "alpha": "1.000"
1212 }
1313 }
@@ -41,9 +41,9 @@
4141 "color": {
4242 "color-space": "srgb",
4343 "components": {
44 "red": "0x0B",
45 "green": "0x50",
46 "blue": "0x23",
44 "red": "0x00",
45 "green": "0x66",
46 "blue": "0x1F",
4747 "alpha": "1.000"
4848 }
4949 }
Shared/Colors.xcassets/StatusPositiveSurface.colorset/Contents.json added +78
@@ -0,0 +1,78 @@
1{
2 "colors": [
3 {
4 "idiom": "universal",
5 "color": {
6 "color-space": "srgb",
7 "components": {
8 "red": "0xE3",
9 "green": "0xF7",
10 "blue": "0xEC",
11 "alpha": "1.000"
12 }
13 }
14 },
15 {
16 "idiom": "universal",
17 "appearances": [
18 {
19 "appearance": "luminosity",
20 "value": "dark"
21 }
22 ],
23 "color": {
24 "color-space": "srgb",
25 "components": {
26 "red": "0x0E",
27 "green": "0x2A",
28 "blue": "0x18",
29 "alpha": "1.000"
30 }
31 }
32 },
33 {
34 "idiom": "universal",
35 "appearances": [
36 {
37 "appearance": "contrast",
38 "value": "high"
39 }
40 ],
41 "color": {
42 "color-space": "srgb",
43 "components": {
44 "red": "0xD3",
45 "green": "0xF0",
46 "blue": "0xE0",
47 "alpha": "1.000"
48 }
49 }
50 },
51 {
52 "idiom": "universal",
53 "appearances": [
54 {
55 "appearance": "luminosity",
56 "value": "dark"
57 },
58 {
59 "appearance": "contrast",
60 "value": "high"
61 }
62 ],
63 "color": {
64 "color-space": "srgb",
65 "components": {
66 "red": "0x12",
67 "green": "0x3A",
68 "blue": "0x20",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}
Shared/Colors.xcassets/StatusWarning.colorset/Contents.json +7 −7
@@ -5,8 +5,8 @@
55 "color": {
66 "color-space": "srgb",
77 "components": {
8 "red": "0x7A",
9 "green": "0x56",
8 "red": "0xAD",
9 "green": "0x51",
1010 "blue": "0x00",
1111 "alpha": "1.000"
1212 }
@@ -24,7 +24,7 @@
2424 "color-space": "srgb",
2525 "components": {
2626 "red": "0xFF",
27 "green": "0xD6",
27 "green": "0x9F",
2828 "blue": "0x0A",
2929 "alpha": "1.000"
3030 }
@@ -41,8 +41,8 @@
4141 "color": {
4242 "color-space": "srgb",
4343 "components": {
44 "red": "0x5C",
45 "green": "0x41",
44 "red": "0x8A",
45 "green": "0x3F",
4646 "blue": "0x00",
4747 "alpha": "1.000"
4848 }
@@ -64,8 +64,8 @@
6464 "color-space": "srgb",
6565 "components": {
6666 "red": "0xFF",
67 "green": "0xE5",
68 "blue": "0x66",
67 "green": "0xBF",
68 "blue": "0x5C",
6969 "alpha": "1.000"
7070 }
7171 }
Shared/Colors.xcassets/StatusWarningSurface.colorset/Contents.json added +78
@@ -0,0 +1,78 @@
1{
2 "colors": [
3 {
4 "idiom": "universal",
5 "color": {
6 "color-space": "srgb",
7 "components": {
8 "red": "0xFB",
9 "green": "0xEC",
10 "blue": "0xDF",
11 "alpha": "1.000"
12 }
13 }
14 },
15 {
16 "idiom": "universal",
17 "appearances": [
18 {
19 "appearance": "luminosity",
20 "value": "dark"
21 }
22 ],
23 "color": {
24 "color-space": "srgb",
25 "components": {
26 "red": "0x2E",
27 "green": "0x1F",
28 "blue": "0x08",
29 "alpha": "1.000"
30 }
31 }
32 },
33 {
34 "idiom": "universal",
35 "appearances": [
36 {
37 "appearance": "contrast",
38 "value": "high"
39 }
40 ],
41 "color": {
42 "color-space": "srgb",
43 "components": {
44 "red": "0xF7",
45 "green": "0xE0",
46 "blue": "0xC9",
47 "alpha": "1.000"
48 }
49 }
50 },
51 {
52 "idiom": "universal",
53 "appearances": [
54 {
55 "appearance": "luminosity",
56 "value": "dark"
57 },
58 {
59 "appearance": "contrast",
60 "value": "high"
61 }
62 ],
63 "color": {
64 "color-space": "srgb",
65 "components": {
66 "red": "0x3D",
67 "green": "0x2A",
68 "blue": "0x0B",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}