diff options
| author | Christian Cleberg <[email protected]> | 2026-07-20 19:14:04 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-20 21:03:10 -0500 |
| commit | 2c608ed9cb11f3e835f577994b5627b9b7910f4f (patch) | |
| tree | 078ff50c5151fcc5e91b9d28051562f0bc28088d /DomainDig/DomainDigUI.swift | |
| parent | 349bec0e99f330fc1172ca9847c58f019e861f61 (diff) | |
| download | domain-dig-2c608ed9cb11f3e835f577994b5627b9b7910f4f.tar.gz domain-dig-2c608ed9cb11f3e835f577994b5627b9b7910f4f.tar.bz2 domain-dig-2c608ed9cb11f3e835f577994b5627b9b7910f4f.zip | |
feat(a11y): unlock light mode and add appearance preference (#21 phase 2)
Removes the 16 scattered .preferredColorScheme(.dark) calls and the one
.toolbarColorScheme, and applies appearance in exactly one place — the
WindowGroup in DomainDigApp. Re-applying per view is what let the lock
spread across eight files unnoticed until light mode was unreachable.
Adds AppAppearance (System / Light / Dark) in @AppStorage, exposed under
Settings > Display next to Density. Honouring the system setting and
offering an override is one key, and it keeps the deliberate dark
aesthetic reachable for anyone who wants it.
Also replaces .secondary with AppTextSecondary across 191 sites. iOS's
own secondaryLabel is 3.29:1 on a light card — below AA — which never
showed while the app was locked to dark, where the same colour reads
6.32:1. Unlocking light mode is precisely what exposed it, so it belongs
here rather than in a later phase: without it, light mode would ship with
body text under 4.5:1 app-wide.
Dark mode reports 18 findings, unchanged from phase 1 — no regression
from unlocking. Light mode reports 21. The three extra are iOS-rendered
Section headers (TIER, PREFERENCES, SERVICES) using the system's grey;
overriding system header styling across every section to gain ~0.3:1 on
decorative labels is a poor trade and is left alone.
Two long-standing Settings contrast findings are now explained. They are
the last rows of a section sitting under the translucent tab bar, so the
audit measures text against a blended background — confirmed by
screenshot, present in dark mode since phase 0, and standard iOS
scroll-under behaviour rather than a defect.
Diffstat (limited to 'DomainDig/DomainDigUI.swift')
| -rw-r--r-- | DomainDig/DomainDigUI.swift | 55 |
1 files changed, 47 insertions, 8 deletions
diff --git a/DomainDig/DomainDigUI.swift b/DomainDig/DomainDigUI.swift index 9c63e73..7d99e4e 100644 --- a/DomainDig/DomainDigUI.swift +++ b/DomainDig/DomainDigUI.swift @@ -6,6 +6,45 @@ import UIKit import AppKit #endif +/// User-selected appearance, applied once at the `WindowGroup`. +/// +/// Deliberately applied in exactly one place. The app previously carried 16 +/// separate `.preferredColorScheme(.dark)` calls scattered across view bodies, +/// which is how it became impossible to reach light mode at all — re-applying +/// per view is what let the lock spread unnoticed. +enum AppAppearance: String, CaseIterable, Identifiable { + case system + case light + case dark + + static let userDefaultsKey = "appAppearance" + + var id: String { rawValue } + + var title: String { + switch self { + case .system: + return "System" + case .light: + return "Light" + case .dark: + return "Dark" + } + } + + /// `nil` hands control back to the system setting. + var colorScheme: ColorScheme? { + switch self { + case .system: + return nil + case .light: + return .light + case .dark: + return .dark + } + } +} + enum AppDensity: String, CaseIterable, Identifiable { case compact case comfortable @@ -96,7 +135,7 @@ enum AppStatusFactory { case .registered: return .init(title: "Registered", systemImage: "circle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) case .unknown, .none: - return .init(title: "Unknown", systemImage: "questionmark.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) + return .init(title: "Unknown", systemImage: "questionmark.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) } } @@ -112,7 +151,7 @@ enum AppStatusFactory { static func email(_ result: EmailSecurityResult?, error: String?) -> AppStatusBadgeModel { guard error == nil, let result else { - return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) + return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) } let foundCount = [result.spf.found, result.dmarc.found, result.dkim.found].filter { $0 }.count @@ -122,18 +161,18 @@ enum AppStatusFactory { case 1, 2: return .init(title: "Partial", systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) default: - return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) + return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) } } static func change(_ summary: DomainChangeSummary?) -> AppStatusBadgeModel { guard let summary else { - return .init(title: "Unchanged", systemImage: "circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) + 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: "Unchanged", systemImage: "checkmark.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) + return .init(title: "Unchanged", systemImage: "checkmark.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)) } } @@ -259,7 +298,7 @@ struct EmptyStateCardView: View { Text(message) .font(appDensity.font(.body)) - .foregroundStyle(.secondary) + .foregroundStyle(Color(.appTextSecondary)) .fixedSize(horizontal: false, vertical: true) Text(suggestion) @@ -311,14 +350,14 @@ struct CollapsibleSectionView<HeaderTrailing: View, Content: View>: View { if let subtitle { Text(subtitle) .font(appDensity.font(.caption)) - .foregroundStyle(.secondary) + .foregroundStyle(Color(.appTextSecondary)) } } Spacer(minLength: 8) trailing() Image(systemName: isCollapsed ? "chevron.down" : "chevron.up") .font(.caption.weight(.semibold)) - .foregroundStyle(.secondary) + .foregroundStyle(Color(.appTextSecondary)) } .contentShape(Rectangle()) .frame(minHeight: appDensity.metrics.controlMinHeight, alignment: .center) |
