summaryrefslogtreecommitdiff
path: root/DomainDig/DomainDigUI.swift
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 /DomainDig/DomainDigUI.swift
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.
Diffstat (limited to 'DomainDig/DomainDigUI.swift')
-rw-r--r--DomainDig/DomainDigUI.swift63
1 files changed, 55 insertions, 8 deletions
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))
}