aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-20 20:25:01 -0500
committerChristian Cleberg <[email protected]>2026-07-20 21:03:10 -0500
commit9e58a36915bb1fe8cf88423c9519c0c12dbbf2e2 (patch)
treea9397c168adcdb63deda80440511063ef57732f4
parent2c608ed9cb11f3e835f577994b5627b9b7910f4f (diff)
downloaddomain-dig-9e58a36915bb1fe8cf88423c9519c0c12dbbf2e2.tar.gz
domain-dig-9e58a36915bb1fe8cf88423c9519c0c12dbbf2e2.tar.bz2
domain-dig-9e58a36915bb1fe8cf88423c9519c0c12dbbf2e2.zip
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.
-rw-r--r--Docs/ACCESSIBILITY.md36
-rw-r--r--DomainDig/BatchResultsView.swift16
-rw-r--r--DomainDig/ContentView.swift60
-rw-r--r--DomainDig/DashboardView.swift24
-rw-r--r--DomainDig/DomainDig/AuditViews.swift20
-rw-r--r--DomainDig/DomainDigUI.swift63
-rw-r--r--DomainDig/HistoryView.swift2
-rw-r--r--DomainDig/MonitoringView.swift10
-rw-r--r--DomainDig/TimelineView.swift2
-rw-r--r--DomainDig/WatchlistView.swift8
-rw-r--r--Shared/Colors.xcassets/StatusCritical.colorset/Contents.json12
-rw-r--r--Shared/Colors.xcassets/StatusCriticalSurface.colorset/Contents.json78
-rw-r--r--Shared/Colors.xcassets/StatusInfoSurface.colorset/Contents.json78
-rw-r--r--Shared/Colors.xcassets/StatusNeutralSurface.colorset/Contents.json78
-rw-r--r--Shared/Colors.xcassets/StatusPositive.colorset/Contents.json12
-rw-r--r--Shared/Colors.xcassets/StatusPositiveSurface.colorset/Contents.json78
-rw-r--r--Shared/Colors.xcassets/StatusWarning.colorset/Contents.json14
-rw-r--r--Shared/Colors.xcassets/StatusWarningSurface.colorset/Contents.json78
18 files changed, 566 insertions, 103 deletions
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
+ }
+}