Commit 2c608ed9cb

2c608ed9cb11f3e835f577994b5627b9b7910f4f

parent: 349bec0e99

Unsigned

cmc <hello@cleberg.net> · 2026-07-21 00:14 UTC

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.

Layout: unified · split

Docs/ACCESSIBILITY.md +26
@@ -29,6 +29,12 @@ worst of those three is shown:
29| `StatusWarning` | `#7A5600` | `#FFD60A` | 4.74 | 9.61 | 29| `StatusWarning` | `#7A5600` | `#FFD60A` | 4.74 | 9.61 |
30| `StatusCritical` | `#B3261E` | `#FF6961` | 4.51 | 5.54 | 30| `StatusCritical` | `#B3261E` | `#FF6961` | 4.51 | 5.54 |
31| `StatusNeutral` | `#5A5A5F` | `#A1A1A6` | 4.92 | 5.88 | 31| `StatusNeutral` | `#5A5A5F` | `#A1A1A6` | 4.92 | 5.88 |
32| `AppTextSecondary` | `#5A5A5F` | `#A1A1A6` | 6.15 | 7.50 |
33
34`AppTextSecondary` replaces `.secondary` for body text. iOS's own `secondaryLabel`
35is only **3.29:1** on a light card — below AA — which never showed while the app
36was locked to dark, where the same colour reads 6.32:1. Unlocking light mode
37exposed it across 191 sites.
32 38
33High Contrast variants push further in the same direction. Surfaces 39High Contrast variants push further in the same direction. Surfaces
34(`AppBackground`, `AppSurface`, `AppSurfaceElevated`, `AppSeparator`) carry no 40(`AppBackground`, `AppSurface`, `AppSurfaceElevated`, `AppSeparator`) carry no
@@ -58,6 +64,26 @@ So there are two colours:
58`AppOnAccent` is the label colour for a solid accent fill and flips by scheme — 64`AppOnAccent` is the label colour for a solid accent fill and flips by scheme —
59white on the light accent, black on the dark one. 65white on the light accent, black on the dark one.
60 66
67## Appearance
68
69`AppAppearance` (System / Light / Dark) is stored in `@AppStorage` and applied in
70**exactly one place** — the `WindowGroup` in `DomainDigApp`. Keep it that way. The
71app previously carried 16 separate `.preferredColorScheme(.dark)` calls scattered
72through view bodies, which is how light mode became unreachable without anyone
73noticing; re-applying per view is what let the lock spread.
74
75Users override it under Settings → Display.
76
77### Known light-mode findings
78
79| Finding | Cause | Action |
80| --- | --- | --- |
81| 2× `contrast failed` on Settings | The last rows of a section sit under the translucent tab bar, so the audit measures text against a blended background. Present in dark mode too, since phase 0. | None — standard iOS scroll-under behaviour |
82| 3× `contrast nearly passed` on Settings | iOS-rendered `Section` headers (`TIER`, `PREFERENCES`, `SERVICES`) use the system's grey. | Not fixed. Overriding system header styling across every section to gain ~0.3:1 on decorative labels trades platform convention for very little |
83
84Dark mode reports 18 findings and light mode 21; the three extra are the section
85headers above. Everything the app actually controls passes in both schemes.
86
61## Findings are reported, not enforced 87## Findings are reported, not enforced
62 88
63The audit surfaces violations that exist today, so failing on all of them would 89The audit surfaces violations that exist today, so failing on all of them would
DomainDig/AuditModeView.swift +2 −2
@@ -25,7 +25,7 @@ struct AuditModeView: View {
25 Section("Audit Timeline") { 25 Section("Audit Timeline") {
26 if auditStore.sessions.isEmpty { 26 if auditStore.sessions.isEmpty {
27 Text("No audits yet.") 27 Text("No audits yet.")
28 .foregroundStyle(.secondary) 28 .foregroundStyle(Color(.appTextSecondary))
29 } else { 29 } else {
30 ForEach(auditStore.sessions) { session in 30 ForEach(auditStore.sessions) { session in
31 Button { 31 Button {
@@ -35,7 +35,7 @@ struct AuditModeView: View {
35 Text(session.domain).font(.headline) 35 Text(session.domain).font(.headline)
36 Text("\(session.createdAt.formatted(date: .abbreviated, time: .shortened)) • \(session.status.title)") 36 Text("\(session.createdAt.formatted(date: .abbreviated, time: .shortened)) • \(session.status.title)")
37 .font(.caption) 37 .font(.caption)
38 .foregroundStyle(.secondary) 38 .foregroundStyle(Color(.appTextSecondary))
39 } 39 }
40 } 40 }
41 } 41 }
DomainDig/BatchResultsView.swift +8 −8
@@ -17,12 +17,12 @@ struct BatchResultsView: View {
17 .frame(width: 120) 17 .frame(width: 120)
18 Text(viewModel.batchProgressLabel) 18 Text(viewModel.batchProgressLabel)
19 .font(appDensity.font(.caption2)) 19 .font(appDensity.font(.caption2))
20 .foregroundStyle(.secondary) 20 .foregroundStyle(Color(.appTextSecondary))
21 } 21 }
22 } else if !viewModel.batchResults.isEmpty { 22 } else if !viewModel.batchResults.isEmpty {
23 Text("\(viewModel.batchResults.count) domains") 23 Text("\(viewModel.batchResults.count) domains")
24 .font(appDensity.font(.caption2)) 24 .font(appDensity.font(.caption2))
25 .foregroundStyle(.secondary) 25 .foregroundStyle(Color(.appTextSecondary))
26 } 26 }
27 } 27 }
28 28
@@ -67,7 +67,7 @@ struct BatchResultRowView: View {
67 Spacer(minLength: 8) 67 Spacer(minLength: 8)
68 Text(result.resultSource.label.lowercased()) 68 Text(result.resultSource.label.lowercased())
69 .font(appDensity.font(.caption2)) 69 .font(appDensity.font(.caption2))
70 .foregroundStyle(.secondary) 70 .foregroundStyle(Color(.appTextSecondary))
71 AppStatusBadgeView(model: quickStatusBadge) 71 AppStatusBadgeView(model: quickStatusBadge)
72 } 72 }
73 73
@@ -80,7 +80,7 @@ struct BatchResultRowView: View {
80 } 80 }
81 } 81 }
82 .font(appDensity.font(.caption2)) 82 .font(appDensity.font(.caption2))
83 .foregroundStyle(.secondary) 83 .foregroundStyle(Color(.appTextSecondary))
84 84
85 HStack(spacing: 10) { 85 HStack(spacing: 10) {
86 Text(result.primaryIP ?? "No IP") 86 Text(result.primaryIP ?? "No IP")
@@ -93,12 +93,12 @@ struct BatchResultRowView: View {
93 .lineLimit(1) 93 .lineLimit(1)
94 } 94 }
95 .font(appDensity.font(.caption2)) 95 .font(appDensity.font(.caption2))
96 .foregroundStyle(.secondary) 96 .foregroundStyle(Color(.appTextSecondary))
97 97
98 if let summaryMessage = result.summaryMessage { 98 if let summaryMessage = result.summaryMessage {
99 Text(summaryMessage) 99 Text(summaryMessage)
100 .font(appDensity.font(.caption2)) 100 .font(appDensity.font(.caption2))
101 .foregroundStyle(.secondary) 101 .foregroundStyle(Color(.appTextSecondary))
102 } 102 }
103 103
104 if let changeClassification = result.changeClassification { 104 if let changeClassification = result.changeClassification {
@@ -137,7 +137,7 @@ struct BatchResultRowView: View {
137 return .init( 137 return .init(
138 title: availabilityText, 138 title: availabilityText,
139 systemImage: "exclamationmark.circle", 139 systemImage: "exclamationmark.circle",
140 foregroundColor: .secondary, 140 foregroundColor: Color(.appTextSecondary),
141 backgroundColor: Color(.appSurfaceElevated) 141 backgroundColor: Color(.appSurfaceElevated)
142 ) 142 )
143 } 143 }
@@ -145,7 +145,7 @@ struct BatchResultRowView: View {
145 private var quickStatusBadge: AppStatusBadgeModel { 145 private var quickStatusBadge: AppStatusBadgeModel {
146 switch result.status { 146 switch result.status {
147 case .pending: 147 case .pending:
148 return .init(title: "Pending", systemImage: "clock", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) 148 return .init(title: "Pending", systemImage: "clock", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
149 case .running: 149 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(.statusInfo).opacity(0.16))
151 case .completed: 151 case .completed:
DomainDig/BatchSweepSummaryView.swift +2 −2
@@ -32,7 +32,7 @@ struct BatchSweepSummaryView: View {
32 if visibleResults.isEmpty { 32 if visibleResults.isEmpty {
33 Text("No domains with changes or warnings") 33 Text("No domains with changes or warnings")
34 .font(.system(.caption, design: .monospaced)) 34 .font(.system(.caption, design: .monospaced))
35 .foregroundStyle(.secondary) 35 .foregroundStyle(Color(.appTextSecondary))
36 } else { 36 } else {
37 ForEach(visibleResults) { result in 37 ForEach(visibleResults) { result in
38 if let entry = viewModel.historyEntry(for: result) { 38 if let entry = viewModel.historyEntry(for: result) {
@@ -56,7 +56,7 @@ struct BatchSweepSummaryView: View {
56 HStack { 56 HStack {
57 Text(label) 57 Text(label)
58 .font(.system(.caption, design: .monospaced)) 58 .font(.system(.caption, design: .monospaced))
59 .foregroundStyle(.secondary) 59 .foregroundStyle(Color(.appTextSecondary))
60 Spacer() 60 Spacer()
61 Text(value) 61 Text(value)
62 .font(.system(.callout, design: .monospaced)) 62 .font(.system(.callout, design: .monospaced))
DomainDig/ContentView.swift +77 −72
@@ -173,8 +173,6 @@ struct ContentView: View {
173 ) 173 )
174 ) 174 )
175 .navigationTitle("DomainDig") 175 .navigationTitle("DomainDig")
176 .toolbarColorScheme(.dark, for: .navigationBar)
177 .preferredColorScheme(.dark)
178 .toolbar { 176 .toolbar {
179 ToolbarItem(placement: .topBarTrailing) { 177 ToolbarItem(placement: .topBarTrailing) {
180 if viewModel.hasRun { 178 if viewModel.hasRun {
@@ -182,7 +180,7 @@ struct ContentView: View {
182 viewModel.reset() 180 viewModel.reset()
183 } label: { 181 } label: {
184 Image(systemName: "xmark.circle") 182 Image(systemName: "xmark.circle")
185 .foregroundStyle(.secondary) 183 .foregroundStyle(Color(.appTextSecondary))
186 } 184 }
187 } 185 }
188 } 186 }
@@ -327,12 +325,12 @@ struct ContentView: View {
327 VStack(alignment: .leading, spacing: appDensity.metrics.cardSpacing) { 325 VStack(alignment: .leading, spacing: appDensity.metrics.cardSpacing) {
328 Text("Paste domains separated by new lines or commas.") 326 Text("Paste domains separated by new lines or commas.")
329 .font(appDensity.font(.caption)) 327 .font(appDensity.font(.caption))
330 .foregroundStyle(.secondary) 328 .foregroundStyle(Color(.appTextSecondary))
331 329
332 if let batchAllowanceSummary = FeatureAccessService.batchAllowanceSummary() { 330 if let batchAllowanceSummary = FeatureAccessService.batchAllowanceSummary() {
333 Text(batchAllowanceSummary) 331 Text(batchAllowanceSummary)
334 .font(appDensity.font(.caption2)) 332 .font(appDensity.font(.caption2))
335 .foregroundStyle(.secondary) 333 .foregroundStyle(Color(.appTextSecondary))
336 } 334 }
337 335
338 TextField( 336 TextField(
@@ -544,7 +542,7 @@ struct ContentView: View {
544 } label: { 542 } label: {
545 Image(systemName: "bolt.circle") 543 Image(systemName: "bolt.circle")
546 .font(appDensity.font(.body, design: .default)) 544 .font(appDensity.font(.body, design: .default))
547 .foregroundStyle(.secondary) 545 .foregroundStyle(Color(.appTextSecondary))
548 } 546 }
549 Button { 547 Button {
550 viewModel.toggleSavedDomain() 548 viewModel.toggleSavedDomain()
@@ -583,7 +581,7 @@ struct ContentView: View {
583 } label: { 581 } label: {
584 Image(systemName: "square.and.arrow.up") 582 Image(systemName: "square.and.arrow.up")
585 .font(appDensity.font(.body, design: .default)) 583 .font(appDensity.font(.body, design: .default))
586 .foregroundStyle(.secondary) 584 .foregroundStyle(Color(.appTextSecondary))
587 } 585 }
588 } 586 }
589 } 587 }
@@ -652,13 +650,13 @@ struct ContentView: View {
652 HStack { 650 HStack {
653 Text("RECENT") 651 Text("RECENT")
654 .font(appDensity.font(.caption2)) 652 .font(appDensity.font(.caption2))
655 .foregroundStyle(.secondary) 653 .foregroundStyle(Color(.appTextSecondary))
656 Spacer() 654 Spacer()
657 Button("Clear") { 655 Button("Clear") {
658 viewModel.clearRecentSearches() 656 viewModel.clearRecentSearches()
659 } 657 }
660 .font(appDensity.font(.caption2)) 658 .font(appDensity.font(.caption2))
661 .foregroundStyle(.secondary) 659 .foregroundStyle(Color(.appTextSecondary))
662 } 660 }
663 661
664 ForEach(viewModel.recentSearches, id: \.self) { domain in 662 ForEach(viewModel.recentSearches, id: \.self) { domain in
@@ -694,7 +692,7 @@ struct ContentView: View {
694 .font(appDensity.font(.headline, design: .default, weight: .semibold)) 692 .font(appDensity.font(.headline, design: .default, weight: .semibold))
695 Text(message) 693 Text(message)
696 .font(appDensity.font(.callout, design: .default)) 694 .font(appDensity.font(.callout, design: .default))
697 .foregroundStyle(.secondary) 695 .foregroundStyle(Color(.appTextSecondary))
698 } 696 }
699 .frame(maxWidth: .infinity, alignment: .leading) 697 .frame(maxWidth: .infinity, alignment: .leading)
700 .padding(appDensity.metrics.cardPadding) 698 .padding(appDensity.metrics.cardPadding)
@@ -784,7 +782,7 @@ struct SummaryView: View {
784 VStack(alignment: .leading, spacing: appDensity.metrics.rowSpacing) { 782 VStack(alignment: .leading, spacing: appDensity.metrics.rowSpacing) {
785 Text(field.label) 783 Text(field.label)
786 .font(appDensity.font(.caption2)) 784 .font(appDensity.font(.caption2))
787 .foregroundStyle(.secondary) 785 .foregroundStyle(Color(.appTextSecondary))
788 Text(field.value) 786 Text(field.value)
789 .font(appDensity.font(.caption)) 787 .font(appDensity.font(.caption))
790 .foregroundStyle(ResultColors.color(for: field.tone)) 788 .foregroundStyle(ResultColors.color(for: field.tone))
@@ -826,13 +824,13 @@ struct RiskSummaryCardView: View {
826 Spacer() 824 Spacer()
827 Text("Deterministic") 825 Text("Deterministic")
828 .font(appDensity.font(.caption2)) 826 .font(appDensity.font(.caption2))
829 .foregroundStyle(.secondary) 827 .foregroundStyle(Color(.appTextSecondary))
830 } 828 }
831 829
832 if topFactors.isEmpty { 830 if topFactors.isEmpty {
833 Text("No major risk factors identified") 831 Text("No major risk factors identified")
834 .font(appDensity.font(.caption)) 832 .font(appDensity.font(.caption))
835 .foregroundStyle(.secondary) 833 .foregroundStyle(Color(.appTextSecondary))
836 } else { 834 } else {
837 ForEach(Array(topFactors.enumerated()), id: \.offset) { _, factor in 835 ForEach(Array(topFactors.enumerated()), id: \.offset) { _, factor in
838 HStack(alignment: .top, spacing: 8) { 836 HStack(alignment: .top, spacing: 8) {
@@ -884,7 +882,7 @@ struct InsightsSummaryCardView: View {
884 if insights.isEmpty { 882 if insights.isEmpty {
885 Text("No deterministic insights triggered") 883 Text("No deterministic insights triggered")
886 .font(appDensity.font(.caption)) 884 .font(appDensity.font(.caption))
887 .foregroundStyle(.secondary) 885 .foregroundStyle(Color(.appTextSecondary))
888 } else { 886 } else {
889 ForEach(Array(insights.enumerated()), id: \.offset) { _, insight in 887 ForEach(Array(insights.enumerated()), id: \.offset) { _, insight in
890 HStack(alignment: .top, spacing: 8) { 888 HStack(alignment: .top, spacing: 8) {
@@ -940,7 +938,7 @@ struct StickyLookupSummaryView: View {
940 HStack(spacing: 8) { 938 HStack(spacing: 8) {
941 Label(primaryIP, systemImage: "network") 939 Label(primaryIP, systemImage: "network")
942 .font(appDensity.font(.caption)) 940 .font(appDensity.font(.caption))
943 .foregroundStyle(.secondary) 941 .foregroundStyle(Color(.appTextSecondary))
944 Spacer(minLength: 6) 942 Spacer(minLength: 6)
945 AppCopyButton(value: primaryIP, label: "Copy IP") 943 AppCopyButton(value: primaryIP, label: "Copy IP")
946 } 944 }
@@ -966,7 +964,7 @@ struct LookupProgressOverviewView: View {
966 .foregroundStyle(.primary) 964 .foregroundStyle(.primary)
967 Text(steps.isEmpty ? "Preparing requests" : steps.joined(separator: " • ")) 965 Text(steps.isEmpty ? "Preparing requests" : steps.joined(separator: " • "))
968 .font(appDensity.font(.caption2)) 966 .font(appDensity.font(.caption2))
969 .foregroundStyle(.secondary) 967 .foregroundStyle(Color(.appTextSecondary))
970 } 968 }
971 Spacer() 969 Spacer()
972 } 970 }
@@ -1049,13 +1047,13 @@ struct DomainChangeSummaryView: View {
1049 .clipShape(Capsule()) 1047 .clipShape(Capsule())
1050 Text(summary.generatedAt, style: .time) 1048 Text(summary.generatedAt, style: .time)
1051 .font(appDensity.font(.caption2)) 1049 .font(appDensity.font(.caption2))
1052 .foregroundStyle(.secondary) 1050 .foregroundStyle(Color(.appTextSecondary))
1053 } 1051 }
1054 1052
1055 VStack(alignment: .leading, spacing: 4) { 1053 VStack(alignment: .leading, spacing: 4) {
1056 Text("Inference") 1054 Text("Inference")
1057 .font(appDensity.font(.caption2)) 1055 .font(appDensity.font(.caption2))
1058 .foregroundStyle(.secondary) 1056 .foregroundStyle(Color(.appTextSecondary))
1059 Text(summary.message) 1057 Text(summary.message)
1060 .font(appDensity.font(.caption)) 1058 .font(appDensity.font(.caption))
1061 .foregroundStyle(.primary) 1059 .foregroundStyle(.primary)
@@ -1069,7 +1067,7 @@ struct DomainChangeSummaryView: View {
1069 VStack(alignment: .leading, spacing: 4) { 1067 VStack(alignment: .leading, spacing: 4) {
1070 Text("Observed") 1068 Text("Observed")
1071 .font(appDensity.font(.caption2)) 1069 .font(appDensity.font(.caption2))
1072 .foregroundStyle(.secondary) 1070 .foregroundStyle(Color(.appTextSecondary))
1073 ForEach(Array(summary.observedFacts.enumerated()), id: \.offset) { _, fact in 1071 ForEach(Array(summary.observedFacts.enumerated()), id: \.offset) { _, fact in
1074 Text(fact) 1072 Text(fact)
1075 .font(appDensity.font(.caption)) 1073 .font(appDensity.font(.caption))
@@ -1171,7 +1169,7 @@ struct DomainDiffView: View {
1171 HStack { 1169 HStack {
1172 Text(item.label) 1170 Text(item.label)
1173 .font(.system(.caption, design: .monospaced)) 1171 .font(.system(.caption, design: .monospaced))
1174 .foregroundStyle(.secondary) 1172 .foregroundStyle(Color(.appTextSecondary))
1175 Spacer() 1173 Spacer()
1176 Text("\(item.changeType.marker) \(item.severity.title) • \(changeLabel(for: item.changeType))") 1174 Text("\(item.changeType.marker) \(item.severity.title) • \(changeLabel(for: item.changeType))")
1177 .font(.system(.caption2, design: .monospaced)) 1175 .font(.system(.caption2, design: .monospaced))
@@ -1186,10 +1184,10 @@ struct DomainDiffView: View {
1186 VStack(alignment: .leading, spacing: 2) { 1184 VStack(alignment: .leading, spacing: 2) {
1187 Text("Old") 1185 Text("Old")
1188 .font(.system(.caption2, design: .monospaced)) 1186 .font(.system(.caption2, design: .monospaced))
1189 .foregroundStyle(.secondary) 1187 .foregroundStyle(Color(.appTextSecondary))
1190 Text(oldValue) 1188 Text(oldValue)
1191 .font(.system(.caption2, design: .monospaced)) 1189 .font(.system(.caption2, design: .monospaced))
1192 .foregroundStyle(.secondary) 1190 .foregroundStyle(Color(.appTextSecondary))
1193 .textSelection(.enabled) 1191 .textSelection(.enabled)
1194 } 1192 }
1195 } 1193 }
@@ -1198,7 +1196,7 @@ struct DomainDiffView: View {
1198 VStack(alignment: .leading, spacing: 2) { 1196 VStack(alignment: .leading, spacing: 2) {
1199 Text("New") 1197 Text("New")
1200 .font(.system(.caption2, design: .monospaced)) 1198 .font(.system(.caption2, design: .monospaced))
1201 .foregroundStyle(.secondary) 1199 .foregroundStyle(Color(.appTextSecondary))
1202 Text(newValue) 1200 Text(newValue)
1203 .font(.system(.caption, design: .monospaced)) 1201 .font(.system(.caption, design: .monospaced))
1204 .foregroundStyle(item.hasChanges ? .primary : .secondary) 1202 .foregroundStyle(item.hasChanges ? .primary : .secondary)
@@ -1306,7 +1304,7 @@ struct TrackedDomainDetailHeaderView: View {
1306 Text("Last refresh \(trackedDomain.updatedAt.formatted(date: .abbreviated, time: .shortened))") 1304 Text("Last refresh \(trackedDomain.updatedAt.formatted(date: .abbreviated, time: .shortened))")
1307 } 1305 }
1308 .font(.system(.caption2, design: .monospaced)) 1306 .font(.system(.caption2, design: .monospaced))
1309 .foregroundStyle(.secondary) 1307 .foregroundStyle(Color(.appTextSecondary))
1310 } 1308 }
1311 } 1309 }
1312} 1310}
@@ -1394,7 +1392,7 @@ struct DomainSectionView: View {
1394 VStack(alignment: .leading, spacing: 8) { 1392 VStack(alignment: .leading, spacing: 8) {
1395 Text("Part of workflow") 1393 Text("Part of workflow")
1396 .font(appDensity.font(.caption)) 1394 .font(appDensity.font(.caption))
1397 .foregroundStyle(.secondary) 1395 .foregroundStyle(Color(.appTextSecondary))
1398 1396
1399 ForEach(workflows) { workflow in 1397 ForEach(workflows) { workflow in
1400 HStack { 1398 HStack {
@@ -1429,7 +1427,7 @@ struct DomainSectionView: View {
1429 if showSuggestions { 1427 if showSuggestions {
1430 Text("Suggestions") 1428 Text("Suggestions")
1431 .font(appDensity.font(.caption)) 1429 .font(appDensity.font(.caption))
1432 .foregroundStyle(.secondary) 1430 .foregroundStyle(Color(.appTextSecondary))
1433 .padding(.top, 4) 1431 .padding(.top, 4)
1434 if suggestionsLoading { 1432 if suggestionsLoading {
1435 ProgressView("Checking alternatives…") 1433 ProgressView("Checking alternatives…")
@@ -1527,7 +1525,7 @@ struct OwnershipSectionView: View {
1527 HStack { 1525 HStack {
1528 Text("History") 1526 Text("History")
1529 .font(appDensity.font(.caption)) 1527 .font(appDensity.font(.caption))
1530 .foregroundStyle(.secondary) 1528 .foregroundStyle(Color(.appTextSecondary))
1531 Spacer() 1529 Spacer()
1532 if let onLoadHistory, history.isEmpty, !historyLoading, !showsHistoryPlaceholder { 1530 if let onLoadHistory, history.isEmpty, !historyLoading, !showsHistoryPlaceholder {
1533 Button("Load") { 1531 Button("Load") {
@@ -1545,12 +1543,12 @@ struct OwnershipSectionView: View {
1545 VStack(alignment: .leading, spacing: 3) { 1543 VStack(alignment: .leading, spacing: 3) {
1546 Text(event.date.formatted(date: .abbreviated, time: .omitted)) 1544 Text(event.date.formatted(date: .abbreviated, time: .omitted))
1547 .font(appDensity.font(.caption2)) 1545 .font(appDensity.font(.caption2))
1548 .foregroundStyle(.secondary) 1546 .foregroundStyle(Color(.appTextSecondary))
1549 Text(event.summary) 1547 Text(event.summary)
1550 .font(appDensity.font(.caption)) 1548 .font(appDensity.font(.caption))
1551 Text(event.source) 1549 Text(event.source)
1552 .font(appDensity.font(.caption2)) 1550 .font(appDensity.font(.caption2))
1553 .foregroundStyle(.secondary) 1551 .foregroundStyle(Color(.appTextSecondary))
1554 } 1552 }
1555 } 1553 }
1556 } else if let historyError { 1554 } else if let historyError {
@@ -1604,7 +1602,7 @@ struct IntelligenceSectionView: View {
1604 .font(appDensity.font(.caption, weight: .semibold)) 1602 .font(appDensity.font(.caption, weight: .semibold))
1605 Text(signal.detail) 1603 Text(signal.detail)
1606 .font(appDensity.font(.caption2)) 1604 .font(appDensity.font(.caption2))
1607 .foregroundStyle(.secondary) 1605 .foregroundStyle(Color(.appTextSecondary))
1608 } 1606 }
1609 } 1607 }
1610 } 1608 }
@@ -1645,7 +1643,7 @@ struct IntelligenceSectionView: View {
1645 } 1643 }
1646 Text("First \(item.firstSeen.formatted(date: .abbreviated, time: .omitted)) • Last \(item.lastSeen.formatted(date: .abbreviated, time: .omitted)) • Seen \(item.recurrenceCount)x") 1644 Text("First \(item.firstSeen.formatted(date: .abbreviated, time: .omitted)) • Last \(item.lastSeen.formatted(date: .abbreviated, time: .omitted)) • Seen \(item.recurrenceCount)x")
1647 .font(appDensity.font(.caption2)) 1645 .font(appDensity.font(.caption2))
1648 .foregroundStyle(.secondary) 1646 .foregroundStyle(Color(.appTextSecondary))
1649 } 1647 }
1650 } 1648 }
1651 } 1649 }
@@ -1678,7 +1676,7 @@ struct IntelligenceSectionView: View {
1678 VStack(alignment: .leading, spacing: 3) { 1676 VStack(alignment: .leading, spacing: 3) {
1679 Text(date.formatted(date: .abbreviated, time: .omitted)) 1677 Text(date.formatted(date: .abbreviated, time: .omitted))
1680 .font(appDensity.font(.caption2)) 1678 .font(appDensity.font(.caption2))
1681 .foregroundStyle(.secondary) 1679 .foregroundStyle(Color(.appTextSecondary))
1682 Text(title) 1680 Text(title)
1683 .font(appDensity.font(.caption)) 1681 .font(appDensity.font(.caption))
1684 } 1682 }
@@ -1755,7 +1753,7 @@ struct SubdomainsSectionView: View {
1755 if !groups.isEmpty { 1753 if !groups.isEmpty {
1756 Text("Groups") 1754 Text("Groups")
1757 .font(appDensity.font(.caption2)) 1755 .font(appDensity.font(.caption2))
1758 .foregroundStyle(.secondary) 1756 .foregroundStyle(Color(.appTextSecondary))
1759 ForEach(groups) { group in 1757 ForEach(groups) { group in
1760 HStack { 1758 HStack {
1761 Text("\(group.label).*") 1759 Text("\(group.label).*")
@@ -1764,7 +1762,7 @@ struct SubdomainsSectionView: View {
1764 Spacer() 1762 Spacer()
1765 Text("\(group.subdomains.count)") 1763 Text("\(group.subdomains.count)")
1766 .font(appDensity.font(.caption2)) 1764 .font(appDensity.font(.caption2))
1767 .foregroundStyle(.secondary) 1765 .foregroundStyle(Color(.appTextSecondary))
1768 } 1766 }
1769 } 1767 }
1770 } 1768 }
@@ -1898,7 +1896,7 @@ struct DNSSectionView: View {
1898 if let wildcardTitle = section.wildcardTitle { 1896 if let wildcardTitle = section.wildcardTitle {
1899 Text(wildcardTitle) 1897 Text(wildcardTitle)
1900 .font(.system(.caption, design: .monospaced)) 1898 .font(.system(.caption, design: .monospaced))
1901 .foregroundStyle(.secondary) 1899 .foregroundStyle(Color(.appTextSecondary))
1902 .padding(.top, 4) 1900 .padding(.top, 4)
1903 ForEach(section.wildcardRows) { row in 1901 ForEach(section.wildcardRows) { row in
1904 LabeledValueRow(row: row) 1902 LabeledValueRow(row: row)
@@ -1940,18 +1938,18 @@ struct DNSSectionView: View {
1940 VStack(alignment: .leading, spacing: 3) { 1938 VStack(alignment: .leading, spacing: 3) {
1941 Text(event.date.formatted(date: .abbreviated, time: .omitted)) 1939 Text(event.date.formatted(date: .abbreviated, time: .omitted))
1942 .font(appDensity.font(.caption2)) 1940 .font(appDensity.font(.caption2))
1943 .foregroundStyle(.secondary) 1941 .foregroundStyle(Color(.appTextSecondary))
1944 Text(event.summary) 1942 Text(event.summary)
1945 .font(appDensity.font(.caption)) 1943 .font(appDensity.font(.caption))
1946 if !event.aRecords.isEmpty { 1944 if !event.aRecords.isEmpty {
1947 Text("A: \(event.aRecords.joined(separator: ", "))") 1945 Text("A: \(event.aRecords.joined(separator: ", "))")
1948 .font(appDensity.font(.caption2)) 1946 .font(appDensity.font(.caption2))
1949 .foregroundStyle(.secondary) 1947 .foregroundStyle(Color(.appTextSecondary))
1950 } 1948 }
1951 if !event.nameservers.isEmpty { 1949 if !event.nameservers.isEmpty {
1952 Text("NS: \(event.nameservers.joined(separator: ", "))") 1950 Text("NS: \(event.nameservers.joined(separator: ", "))")
1953 .font(appDensity.font(.caption2)) 1951 .font(appDensity.font(.caption2))
1954 .foregroundStyle(.secondary) 1952 .foregroundStyle(Color(.appTextSecondary))
1955 } 1953 }
1956 } 1954 }
1957 } 1955 }
@@ -2017,7 +2015,7 @@ struct WebSectionView: View {
2017 if let sslInfo, !sslInfo.subjectAltNames.isEmpty { 2015 if let sslInfo, !sslInfo.subjectAltNames.isEmpty {
2018 Text("SANs") 2016 Text("SANs")
2019 .font(appDensity.font(.caption2)) 2017 .font(appDensity.font(.caption2))
2020 .foregroundStyle(.secondary) 2018 .foregroundStyle(Color(.appTextSecondary))
2021 ForEach(sslInfo.subjectAltNames, id: \.self) { san in 2019 ForEach(sslInfo.subjectAltNames, id: \.self) { san in
2022 HStack(alignment: .top, spacing: 8) { 2020 HStack(alignment: .top, spacing: 8) {
2023 Text(san) 2021 Text(san)
@@ -2091,7 +2089,7 @@ struct WebSectionView: View {
2091 HStack(alignment: .top, spacing: 6) { 2089 HStack(alignment: .top, spacing: 6) {
2092 Text(redirect.stepLabel) 2090 Text(redirect.stepLabel)
2093 .font(appDensity.font(.caption)) 2091 .font(appDensity.font(.caption))
2094 .foregroundStyle(.secondary) 2092 .foregroundStyle(Color(.appTextSecondary))
2095 .frame(width: 16, alignment: .trailing) 2093 .frame(width: 16, alignment: .trailing)
2096 Text(redirect.statusCode) 2094 Text(redirect.statusCode)
2097 .font(appDensity.font(.caption)) 2095 .font(appDensity.font(.caption))
@@ -2104,7 +2102,7 @@ struct WebSectionView: View {
2104 if redirect.isFinal { 2102 if redirect.isFinal {
2105 Text("(final)") 2103 Text("(final)")
2106 .font(appDensity.font(.caption2)) 2104 .font(appDensity.font(.caption2))
2107 .foregroundStyle(.secondary) 2105 .foregroundStyle(Color(.appTextSecondary))
2108 } 2106 }
2109 } 2107 }
2110 } 2108 }
@@ -2149,7 +2147,7 @@ struct EmailSectionView: View {
2149 if !assessment.reasons.isEmpty { 2147 if !assessment.reasons.isEmpty {
2150 Text(assessment.reasons.joined(separator: " | ")) 2148 Text(assessment.reasons.joined(separator: " | "))
2151 .font(appDensity.font(.caption2)) 2149 .font(appDensity.font(.caption2))
2152 .foregroundStyle(.secondary) 2150 .foregroundStyle(Color(.appTextSecondary))
2153 } 2151 }
2154 } 2152 }
2155 if loading { 2153 if loading {
@@ -2176,7 +2174,7 @@ struct EmailSectionView: View {
2176 if let auxiliaryDetail = row.auxiliaryDetail { 2174 if let auxiliaryDetail = row.auxiliaryDetail {
2177 Text(auxiliaryDetail) 2175 Text(auxiliaryDetail)
2178 .font(appDensity.font(.caption2)) 2176 .font(appDensity.font(.caption2))
2179 .foregroundStyle(.secondary) 2177 .foregroundStyle(Color(.appTextSecondary))
2180 } 2178 }
2181 } 2179 }
2182 } 2180 }
@@ -2192,9 +2190,9 @@ struct EmailSectionView: View {
2192 case .warning: 2190 case .warning:
2193 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(.statusWarning).opacity(0.16))
2194 case .failure: 2192 case .failure:
2195 return .init(title: row.status, systemImage: "minus.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) 2193 return .init(title: row.status, systemImage: "minus.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
2196 case .primary, .secondary: 2194 case .primary, .secondary:
2197 return .init(title: row.status, systemImage: "circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) 2195 return .init(title: row.status, systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
2198 } 2196 }
2199 } 2197 }
2200} 2198}
@@ -2244,7 +2242,7 @@ struct NetworkSectionView: View {
2244 Spacer() 2242 Spacer()
2245 Text(row.latencyLabel) 2243 Text(row.latencyLabel)
2246 .font(appDensity.font(.caption2)) 2244 .font(appDensity.font(.caption2))
2247 .foregroundStyle(.secondary) 2245 .foregroundStyle(Color(.appTextSecondary))
2248 AppStatusBadgeView(model: reachabilityBadge(row)) 2246 AppStatusBadgeView(model: reachabilityBadge(row))
2249 } 2247 }
2250 } 2248 }
@@ -2304,7 +2302,7 @@ struct NetworkSectionView: View {
2304 } else { 2302 } else {
2305 Text("Standard Ports") 2303 Text("Standard Ports")
2306 .font(.system(.caption, design: .monospaced)) 2304 .font(.system(.caption, design: .monospaced))
2307 .foregroundStyle(.secondary) 2305 .foregroundStyle(Color(.appTextSecondary))
2308 PortRowsView(rows: standardPortRows) 2306 PortRowsView(rows: standardPortRows)
2309 } 2307 }
2310 2308
@@ -2353,7 +2351,7 @@ struct NetworkSectionView: View {
2353 case .failure: 2351 case .failure:
2354 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(.statusCritical).opacity(0.16))
2355 case .primary, .secondary: 2353 case .primary, .secondary:
2356 return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) 2354 return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
2357 } 2355 }
2358 } 2356 }
2359} 2357}
@@ -2379,14 +2377,14 @@ struct PortRowsView: View {
2379 if let durationLabel = row.durationLabel { 2377 if let durationLabel = row.durationLabel {
2380 Text(durationLabel) 2378 Text(durationLabel)
2381 .font(appDensity.font(.caption2)) 2379 .font(appDensity.font(.caption2))
2382 .foregroundStyle(.secondary) 2380 .foregroundStyle(Color(.appTextSecondary))
2383 } 2381 }
2384 AppStatusBadgeView(model: portBadge(row)) 2382 AppStatusBadgeView(model: portBadge(row))
2385 } 2383 }
2386 if let banner = row.banner { 2384 if let banner = row.banner {
2387 Text(banner) 2385 Text(banner)
2388 .font(appDensity.font(.caption2)) 2386 .font(appDensity.font(.caption2))
2389 .foregroundStyle(.secondary) 2387 .foregroundStyle(Color(.appTextSecondary))
2390 .padding(.leading, 8) 2388 .padding(.leading, 8)
2391 } 2389 }
2392 } 2390 }
@@ -2404,7 +2402,7 @@ struct PortRowsView: View {
2404 case .failure: 2402 case .failure:
2405 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(.statusCritical).opacity(0.16))
2406 case .primary, .secondary: 2404 case .primary, .secondary:
2407 return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) 2405 return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
2408 } 2406 }
2409 } 2407 }
2410} 2408}
@@ -2512,15 +2510,15 @@ struct SectionTrustMetadataView: View {
2512 if let confidence { 2510 if let confidence {
2513 Text("Confidence \(confidence.title)") 2511 Text("Confidence \(confidence.title)")
2514 .font(appDensity.font(.caption2)) 2512 .font(appDensity.font(.caption2))
2515 .foregroundStyle(.secondary) 2513 .foregroundStyle(Color(.appTextSecondary))
2516 } 2514 }
2517 if let provenance { 2515 if let provenance {
2518 Text(provenance.provider ?? provenance.source) 2516 Text(provenance.provider ?? provenance.source)
2519 .font(appDensity.font(.caption2)) 2517 .font(appDensity.font(.caption2))
2520 .foregroundStyle(.secondary) 2518 .foregroundStyle(Color(.appTextSecondary))
2521 Text(provenance.resultSource.label) 2519 Text(provenance.resultSource.label)
2522 .font(appDensity.font(.caption2)) 2520 .font(appDensity.font(.caption2))
2523 .foregroundStyle(.secondary) 2521 .foregroundStyle(Color(.appTextSecondary))
2524 } 2522 }
2525 } 2523 }
2526 DisclosureGroup("Details") { 2524 DisclosureGroup("Details") {
@@ -2559,7 +2557,7 @@ struct LabeledValueRow: View {
2559 VStack(alignment: .leading, spacing: appDensity.metrics.rowSpacing - 1) { 2557 VStack(alignment: .leading, spacing: appDensity.metrics.rowSpacing - 1) {
2560 Text(row.label) 2558 Text(row.label)
2561 .font(appDensity.font(.caption2)) 2559 .font(appDensity.font(.caption2))
2562 .foregroundStyle(.secondary) 2560 .foregroundStyle(Color(.appTextSecondary))
2563 Text(row.value) 2561 Text(row.value)
2564 .font(appDensity.font(.caption)) 2562 .font(appDensity.font(.caption))
2565 .foregroundStyle(ResultColors.color(for: row.tone)) 2563 .foregroundStyle(ResultColors.color(for: row.tone))
@@ -2656,7 +2654,7 @@ struct SettingsView: View {
2656 if let statusMessage = purchaseService.statusMessage { 2654 if let statusMessage = purchaseService.statusMessage {
2657 Text(statusMessage) 2655 Text(statusMessage)
2658 .font(appDensity.font(.caption, design: .default)) 2656 .font(appDensity.font(.caption, design: .default))
2659 .foregroundStyle(.secondary) 2657 .foregroundStyle(Color(.appTextSecondary))
2660 } 2658 }
2661 2659
2662 if let errorMessage = purchaseService.errorMessage { 2660 if let errorMessage = purchaseService.errorMessage {
@@ -2732,10 +2730,17 @@ struct SettingsView: View {
2732 2730
2733private struct DisplaySettingsView: View { 2731private struct DisplaySettingsView: View {
2734 @AppStorage(AppDensity.userDefaultsKey) private var storedDensity = AppDensity.compact.rawValue 2732 @AppStorage(AppDensity.userDefaultsKey) private var storedDensity = AppDensity.compact.rawValue
2733 @AppStorage(AppAppearance.userDefaultsKey) private var storedAppearance = AppAppearance.system.rawValue
2735 2734
2736 var body: some View { 2735 var body: some View {
2737 Form { 2736 Form {
2738 Section("Display") { 2737 Section("Display") {
2738 Picker("Appearance", selection: $storedAppearance) {
2739 ForEach(AppAppearance.allCases) { appearance in
2740 Text(appearance.title).tag(appearance.rawValue)
2741 }
2742 }
2743
2739 Picker("Density", selection: $storedDensity) { 2744 Picker("Density", selection: $storedDensity) {
2740 ForEach(AppDensity.allCases) { density in 2745 ForEach(AppDensity.allCases) { density in
2741 Text(density.title).tag(density.rawValue) 2746 Text(density.title).tag(density.rawValue)
@@ -2778,7 +2783,7 @@ private struct HistoryNetworkSettingsView: View {
2778 2783
2779 Text("History remains local-first. Auto-prune only trims older local snapshots on this device and defaults to unlimited.") 2784 Text("History remains local-first. Auto-prune only trims older local snapshots on this device and defaults to unlimited.")
2780 .font(appDensity.font(.caption, design: .default)) 2785 .font(appDensity.font(.caption, design: .default))
2781 .foregroundStyle(.secondary) 2786 .foregroundStyle(Color(.appTextSecondary))
2782 } 2787 }
2783 2788
2784 Section("Network") { 2789 Section("Network") {
@@ -2870,11 +2875,11 @@ private struct CloudSyncSettingsView: View {
2870 2875
2871 Text("iCloud Sync stores DomainDig data in your private iCloud account. DomainDig does not operate a sync server. Disabling sync keeps local data on this device.") 2876 Text("iCloud Sync stores DomainDig data in your private iCloud account. DomainDig does not operate a sync server. Disabling sync keeps local data on this device.")
2872 .font(appDensity.font(.caption, design: .default)) 2877 .font(appDensity.font(.caption, design: .default))
2873 .foregroundStyle(.secondary) 2878 .foregroundStyle(Color(.appTextSecondary))
2874 2879
2875 Text(cloudSyncService.detailMessage) 2880 Text(cloudSyncService.detailMessage)
2876 .font(appDensity.font(.caption, design: .default)) 2881 .font(appDensity.font(.caption, design: .default))
2877 .foregroundStyle(.secondary) 2882 .foregroundStyle(Color(.appTextSecondary))
2878 2883
2879 if let lastErrorMessage = cloudSyncService.lastErrorMessage { 2884 if let lastErrorMessage = cloudSyncService.lastErrorMessage {
2880 Text(lastErrorMessage) 2885 Text(lastErrorMessage)
@@ -2932,7 +2937,7 @@ private struct LocalAPISettingsView: View {
2932 if let statusMessage = localAPIService.statusMessage { 2937 if let statusMessage = localAPIService.statusMessage {
2933 Text(statusMessage) 2938 Text(statusMessage)
2934 .font(appDensity.font(.caption, design: .default)) 2939 .font(appDensity.font(.caption, design: .default))
2935 .foregroundStyle(.secondary) 2940 .foregroundStyle(Color(.appTextSecondary))
2936 } 2941 }
2937 } 2942 }
2938 2943
@@ -2947,7 +2952,7 @@ private struct LocalAPISettingsView: View {
2947 2952
2948 Text("Every request requires either `Authorization: Bearer <token>` or `X-API-Token`. DomainDig stores the token in Keychain and only binds the server to localhost.") 2953 Text("Every request requires either `Authorization: Bearer <token>` or `X-API-Token`. DomainDig stores the token in Keychain and only binds the server to localhost.")
2949 .font(appDensity.font(.caption, design: .default)) 2954 .font(appDensity.font(.caption, design: .default))
2950 .foregroundStyle(.secondary) 2955 .foregroundStyle(Color(.appTextSecondary))
2951 } 2956 }
2952 2957
2953 Section("Request Logging") { 2958 Section("Request Logging") {
@@ -2962,7 +2967,7 @@ private struct LocalAPISettingsView: View {
2962 if localAPIService.requestLogs.isEmpty { 2967 if localAPIService.requestLogs.isEmpty {
2963 Text("No local API requests logged yet.") 2968 Text("No local API requests logged yet.")
2964 .font(appDensity.font(.caption, design: .default)) 2969 .font(appDensity.font(.caption, design: .default))
2965 .foregroundStyle(.secondary) 2970 .foregroundStyle(Color(.appTextSecondary))
2966 } else { 2971 } else {
2967 ForEach(localAPIService.requestLogs.prefix(25)) { log in 2972 ForEach(localAPIService.requestLogs.prefix(25)) { log in
2968 VStack(alignment: .leading, spacing: 4) { 2973 VStack(alignment: .leading, spacing: 4) {
@@ -2977,11 +2982,11 @@ private struct LocalAPISettingsView: View {
2977 2982
2978 Text(log.timestamp.formatted(date: .abbreviated, time: .standard)) 2983 Text(log.timestamp.formatted(date: .abbreviated, time: .standard))
2979 .font(appDensity.font(.caption2, design: .default)) 2984 .font(appDensity.font(.caption2, design: .default))
2980 .foregroundStyle(.secondary) 2985 .foregroundStyle(Color(.appTextSecondary))
2981 2986
2982 Text("\(Int(log.duration * 1000)) ms") 2987 Text("\(Int(log.duration * 1000)) ms")
2983 .font(appDensity.font(.caption2, design: .default)) 2988 .font(appDensity.font(.caption2, design: .default))
2984 .foregroundStyle(.secondary) 2989 .foregroundStyle(Color(.appTextSecondary))
2985 } 2990 }
2986 } 2991 }
2987 } 2992 }
@@ -3184,13 +3189,13 @@ private struct MonitoringSettingsView: View {
3184 if let monitoringStatusMessage = viewModel.monitoringStatusMessage { 3189 if let monitoringStatusMessage = viewModel.monitoringStatusMessage {
3185 Text(monitoringStatusMessage) 3190 Text(monitoringStatusMessage)
3186 .font(appDensity.font(.caption, design: .default)) 3191 .font(appDensity.font(.caption, design: .default))
3187 .foregroundStyle(.secondary) 3192 .foregroundStyle(Color(.appTextSecondary))
3188 } 3193 }
3189 3194
3190 if !FeatureAccessService.hasAccess(to: .automatedMonitoring) { 3195 if !FeatureAccessService.hasAccess(to: .automatedMonitoring) {
3191 Text("Background monitoring and alerts are available in Pro.") 3196 Text("Background monitoring and alerts are available in Pro.")
3192 .font(appDensity.font(.caption, design: .default)) 3197 .font(appDensity.font(.caption, design: .default))
3193 .foregroundStyle(.secondary) 3198 .foregroundStyle(Color(.appTextSecondary))
3194 } 3199 }
3195 } 3200 }
3196 } 3201 }
@@ -3260,7 +3265,7 @@ private struct DataPortabilitySettingsView: View {
3260 3265
3261 Text(importMode.explanation) 3266 Text(importMode.explanation)
3262 .font(appDensity.font(.caption, design: .default)) 3267 .font(appDensity.font(.caption, design: .default))
3263 .foregroundStyle(.secondary) 3268 .foregroundStyle(Color(.appTextSecondary))
3264 3269
3265 Button("Export Full Backup") { 3270 Button("Export Full Backup") {
3266 exportFullBackup() 3271 exportFullBackup()
@@ -3317,12 +3322,12 @@ private struct DataPortabilitySettingsView: View {
3317 3322
3318 Text("Data stays on this device unless you export it. Backup files can include domain history, monitoring settings, and notes. Imported files are processed on-device.") 3323 Text("Data stays on this device unless you export it. Backup files can include domain history, monitoring settings, and notes. Imported files are processed on-device.")
3319 .font(appDensity.font(.caption, design: .default)) 3324 .font(appDensity.font(.caption, design: .default))
3320 .foregroundStyle(.secondary) 3325 .foregroundStyle(Color(.appTextSecondary))
3321 3326
3322 if let portabilityStatusMessage = viewModel.portabilityStatusMessage { 3327 if let portabilityStatusMessage = viewModel.portabilityStatusMessage {
3323 Text(portabilityStatusMessage) 3328 Text(portabilityStatusMessage)
3324 .font(appDensity.font(.caption, design: .default)) 3329 .font(appDensity.font(.caption, design: .default))
3325 .foregroundStyle(.secondary) 3330 .foregroundStyle(Color(.appTextSecondary))
3326 } 3331 }
3327 } 3332 }
3328 3333
@@ -3331,7 +3336,7 @@ private struct DataPortabilitySettingsView: View {
3331 Section("Import Debug") { 3336 Section("Import Debug") {
3332 Text(importDebugStatus) 3337 Text(importDebugStatus)
3333 .font(appDensity.font(.caption, design: .default)) 3338 .font(appDensity.font(.caption, design: .default))
3334 .foregroundStyle(.secondary) 3339 .foregroundStyle(Color(.appTextSecondary))
3335 .textSelection(.enabled) 3340 .textSelection(.enabled)
3336 } 3341 }
3337 } 3342 }
@@ -3636,7 +3641,7 @@ private struct DataManagementSettingsView: View {
3636 if let deleteAllSuccessMessage { 3641 if let deleteAllSuccessMessage {
3637 Text(deleteAllSuccessMessage) 3642 Text(deleteAllSuccessMessage)
3638 .font(.footnote.weight(.medium)) 3643 .font(.footnote.weight(.medium))
3639 .foregroundStyle(.secondary) 3644 .foregroundStyle(Color(.appTextSecondary))
3640 .padding(.horizontal, 14) 3645 .padding(.horizontal, 14)
3641 .padding(.vertical, 10) 3646 .padding(.vertical, 10)
3642 .background(.thinMaterial, in: Capsule()) 3647 .background(.thinMaterial, in: Capsule())
@@ -3722,7 +3727,7 @@ private struct DataImportPreviewSheet: View {
3722 Section("Warnings") { 3727 Section("Warnings") {
3723 ForEach(preview.warnings, id: \.self) { warning in 3728 ForEach(preview.warnings, id: \.self) { warning in
3724 Text(warning) 3729 Text(warning)
3725 .foregroundStyle(.secondary) 3730 .foregroundStyle(Color(.appTextSecondary))
3726 } 3731 }
3727 } 3732 }
3728 } 3733 }
DomainDig/DashboardView.swift +7 −8
@@ -139,7 +139,7 @@ struct DashboardView: View {
139 .font(appDensity.font(.headline, design: .default, weight: .semibold)) 139 .font(appDensity.font(.headline, design: .default, weight: .semibold))
140 Text("\(group.domains.count) domain\(group.domains.count == 1 ? "" : "s")") 140 Text("\(group.domains.count) domain\(group.domains.count == 1 ? "" : "s")")
141 .font(appDensity.font(.caption)) 141 .font(appDensity.font(.caption))
142 .foregroundStyle(.secondary) 142 .foregroundStyle(Color(.appTextSecondary))
143 } 143 }
144 Spacer() 144 Spacer()
145 groupBadge(for: group.domains) 145 groupBadge(for: group.domains)
@@ -170,7 +170,6 @@ struct DashboardView: View {
170 } 170 }
171 } 171 }
172 } 172 }
173 .preferredColorScheme(.dark)
174 } 173 }
175 174
176 private func disclosureBinding(for apexDomain: String) -> Binding<Bool> { 175 private func disclosureBinding(for apexDomain: String) -> Binding<Bool> {
@@ -193,7 +192,7 @@ struct DashboardView: View {
193 VStack(alignment: .leading, spacing: 8) { 192 VStack(alignment: .leading, spacing: 8) {
194 Text(title) 193 Text(title)
195 .font(appDensity.font(.caption, design: .default, weight: .semibold)) 194 .font(appDensity.font(.caption, design: .default, weight: .semibold))
196 .foregroundStyle(.secondary) 195 .foregroundStyle(Color(.appTextSecondary))
197 Text("\(value)") 196 Text("\(value)")
198 .font(.system(size: 28, weight: .bold, design: .rounded)) 197 .font(.system(size: 28, weight: .bold, design: .rounded))
199 .foregroundStyle(.primary) 198 .foregroundStyle(.primary)
@@ -230,7 +229,7 @@ struct DashboardView: View {
230 private func dashboardEmptyRow(_ message: String) -> some View { 229 private func dashboardEmptyRow(_ message: String) -> some View {
231 Text(message) 230 Text(message)
232 .font(appDensity.font(.caption)) 231 .font(appDensity.font(.caption))
233 .foregroundStyle(.secondary) 232 .foregroundStyle(Color(.appTextSecondary))
234 .padding(.vertical, 4) 233 .padding(.vertical, 4)
235 } 234 }
236 235
@@ -288,7 +287,7 @@ private struct PortfolioActivityRow: View {
288 Text(relativeTimestamp(item.timestamp)) 287 Text(relativeTimestamp(item.timestamp))
289 } 288 }
290 .font(appDensity.font(.caption2)) 289 .font(appDensity.font(.caption2))
291 .foregroundStyle(.secondary) 290 .foregroundStyle(Color(.appTextSecondary))
292 } 291 }
293 } 292 }
294 .padding(.vertical, 4) 293 .padding(.vertical, 4)
@@ -320,10 +319,10 @@ private struct PortfolioAttentionRow: View {
320 .foregroundStyle(.primary) 319 .foregroundStyle(.primary)
321 Text(item.reason) 320 Text(item.reason)
322 .font(appDensity.font(.caption, design: .default)) 321 .font(appDensity.font(.caption, design: .default))
323 .foregroundStyle(.secondary) 322 .foregroundStyle(Color(.appTextSecondary))
324 Text(relativeTimestamp(item.timestamp)) 323 Text(relativeTimestamp(item.timestamp))
325 .font(appDensity.font(.caption2)) 324 .font(appDensity.font(.caption2))
326 .foregroundStyle(.secondary) 325 .foregroundStyle(Color(.appTextSecondary))
327 } 326 }
328 } 327 }
329 .padding(.vertical, 4) 328 .padding(.vertical, 4)
@@ -353,7 +352,7 @@ private struct PortfolioExpiryRow: View {
353 .foregroundStyle(.primary) 352 .foregroundStyle(.primary)
354 Text(expirySubtitle) 353 Text(expirySubtitle)
355 .font(appDensity.font(.caption)) 354 .font(appDensity.font(.caption))
356 .foregroundStyle(.secondary) 355 .foregroundStyle(Color(.appTextSecondary))
357 } 356 }
358 Spacer() 357 Spacer()
359 AppStatusBadgeView(model: badgeModel) 358 AppStatusBadgeView(model: badgeModel)
DomainDig/DomainCompareView.swift +1 −1
@@ -38,7 +38,7 @@ struct DomainCompareView: View {
38 Section { 38 Section {
39 Text("Choose two different domains to compare.") 39 Text("Choose two different domains to compare.")
40 .font(.callout) 40 .font(.callout)
41 .foregroundStyle(.secondary) 41 .foregroundStyle(Color(.appTextSecondary))
42 } 42 }
43 } 43 }
44 44
DomainDig/DomainDig/AuditViews.swift +17 −19
@@ -41,13 +41,13 @@ struct AuditListView: View {
41 Spacer() 41 Spacer()
42 Text("\(group.sessions.count) audit\(group.sessions.count == 1 ? "" : "s")") 42 Text("\(group.sessions.count) audit\(group.sessions.count == 1 ? "" : "s")")
43 .font(appDensity.font(.caption2)) 43 .font(appDensity.font(.caption2))
44 .foregroundStyle(.secondary) 44 .foregroundStyle(Color(.appTextSecondary))
45 } 45 }
46 46
47 if let latest = group.sessions.first { 47 if let latest = group.sessions.first {
48 Text(latest.findings.isEmpty ? "No findings recorded yet" : latest.findings.map(\.title).prefix(2).joined(separator: " • ")) 48 Text(latest.findings.isEmpty ? "No findings recorded yet" : latest.findings.map(\.title).prefix(2).joined(separator: " • "))
49 .font(appDensity.font(.caption)) 49 .font(appDensity.font(.caption))
50 .foregroundStyle(.secondary) 50 .foregroundStyle(Color(.appTextSecondary))
51 .lineLimit(2) 51 .lineLimit(2)
52 52
53 HStack(spacing: 8) { 53 HStack(spacing: 8) {
@@ -57,7 +57,7 @@ struct AuditListView: View {
57 } 57 }
58 Text("Checklist \(latest.completedChecklistCount)/\(latest.checklist.count)") 58 Text("Checklist \(latest.completedChecklistCount)/\(latest.checklist.count)")
59 .font(appDensity.font(.caption2)) 59 .font(appDensity.font(.caption2))
60 .foregroundStyle(.secondary) 60 .foregroundStyle(Color(.appTextSecondary))
61 } 61 }
62 } 62 }
63 } 63 }
@@ -85,7 +85,6 @@ struct AuditListView: View {
85 } 85 }
86 } 86 }
87 } 87 }
88 .preferredColorScheme(.dark)
89 } 88 }
90} 89}
91 90
@@ -116,7 +115,7 @@ struct AuditDomainTimelineView: View {
116 115
117 Text("Reviewer: \(session.reviewer)") 116 Text("Reviewer: \(session.reviewer)")
118 .font(appDensity.font(.caption)) 117 .font(appDensity.font(.caption))
119 .foregroundStyle(.secondary) 118 .foregroundStyle(Color(.appTextSecondary))
120 119
121 HStack(spacing: 8) { 120 HStack(spacing: 8) {
122 Text("\(session.findings.count) findings") 121 Text("\(session.findings.count) findings")
@@ -126,7 +125,7 @@ struct AuditDomainTimelineView: View {
126 } 125 }
127 } 126 }
128 .font(appDensity.font(.caption2)) 127 .font(appDensity.font(.caption2))
129 .foregroundStyle(.secondary) 128 .foregroundStyle(Color(.appTextSecondary))
130 } 129 }
131 .padding(.vertical, 4) 130 .padding(.vertical, 4)
132 } 131 }
@@ -143,11 +142,11 @@ struct AuditDomainTimelineView: View {
143 Spacer() 142 Spacer()
144 Text("\(point.findingCount) findings") 143 Text("\(point.findingCount) findings")
145 .font(appDensity.font(.caption2)) 144 .font(appDensity.font(.caption2))
146 .foregroundStyle(.secondary) 145 .foregroundStyle(Color(.appTextSecondary))
147 } 146 }
148 Text("Open high severity: \(point.openHighSeverityCount) • Repeated issues: \(point.repeatedIssueCount)") 147 Text("Open high severity: \(point.openHighSeverityCount) • Repeated issues: \(point.repeatedIssueCount)")
149 .font(appDensity.font(.caption2)) 148 .font(appDensity.font(.caption2))
150 .foregroundStyle(.secondary) 149 .foregroundStyle(Color(.appTextSecondary))
151 } 150 }
152 .padding(.vertical, 2) 151 .padding(.vertical, 2)
153 } 152 }
@@ -197,7 +196,7 @@ struct AuditSessionDetailView: View {
197 .font(appDensity.font(.headline, design: .default, weight: .semibold)) 196 .font(appDensity.font(.headline, design: .default, weight: .semibold))
198 Text("Reviewer: \(session.reviewer)") 197 Text("Reviewer: \(session.reviewer)")
199 .font(appDensity.font(.caption)) 198 .font(appDensity.font(.caption))
200 .foregroundStyle(.secondary) 199 .foregroundStyle(Color(.appTextSecondary))
201 } 200 }
202 Spacer() 201 Spacer()
203 auditStatusBadge(session.status) 202 auditStatusBadge(session.status)
@@ -215,7 +214,7 @@ struct AuditSessionDetailView: View {
215 214
216 Text("Captured \(session.evidence.capturedAt.formatted(date: .abbreviated, time: .shortened))") 215 Text("Captured \(session.evidence.capturedAt.formatted(date: .abbreviated, time: .shortened))")
217 .font(appDensity.font(.caption2)) 216 .font(appDensity.font(.caption2))
218 .foregroundStyle(.secondary) 217 .foregroundStyle(Color(.appTextSecondary))
219 } 218 }
220 } 219 }
221 .listRowBackground(Color(.appSurface)) 220 .listRowBackground(Color(.appSurface))
@@ -256,7 +255,7 @@ struct AuditSessionDetailView: View {
256 .foregroundStyle(.primary) 255 .foregroundStyle(.primary)
257 Text(item.detail) 256 Text(item.detail)
258 .font(appDensity.font(.caption)) 257 .font(appDensity.font(.caption))
259 .foregroundStyle(.secondary) 258 .foregroundStyle(Color(.appTextSecondary))
260 } 259 }
261 Spacer() 260 Spacer()
262 } 261 }
@@ -270,7 +269,7 @@ struct AuditSessionDetailView: View {
270 if session.findings.isEmpty { 269 if session.findings.isEmpty {
271 Text("No findings recorded.") 270 Text("No findings recorded.")
272 .font(appDensity.font(.caption)) 271 .font(appDensity.font(.caption))
273 .foregroundStyle(.secondary) 272 .foregroundStyle(Color(.appTextSecondary))
274 } else { 273 } else {
275 ForEach(session.findings) { finding in 274 ForEach(session.findings) { finding in
276 Button { 275 Button {
@@ -286,11 +285,11 @@ struct AuditSessionDetailView: View {
286 } 285 }
287 Text(finding.summary) 286 Text(finding.summary)
288 .font(appDensity.font(.caption)) 287 .font(appDensity.font(.caption))
289 .foregroundStyle(.secondary) 288 .foregroundStyle(Color(.appTextSecondary))
290 .lineLimit(2) 289 .lineLimit(2)
291 Text("\(finding.status.title) • \(finding.evidenceReferences.count) evidence refs") 290 Text("\(finding.status.title) • \(finding.evidenceReferences.count) evidence refs")
292 .font(appDensity.font(.caption2)) 291 .font(appDensity.font(.caption2))
293 .foregroundStyle(.secondary) 292 .foregroundStyle(Color(.appTextSecondary))
294 } 293 }
295 .padding(.vertical, 2) 294 .padding(.vertical, 2)
296 } 295 }
@@ -321,7 +320,7 @@ struct AuditSessionDetailView: View {
321 .font(appDensity.font(.caption, weight: .semibold)) 320 .font(appDensity.font(.caption, weight: .semibold))
322 Text(entry.changeSummaryMessage ?? "No change summary") 321 Text(entry.changeSummaryMessage ?? "No change summary")
323 .font(appDensity.font(.caption2)) 322 .font(appDensity.font(.caption2))
324 .foregroundStyle(.secondary) 323 .foregroundStyle(Color(.appTextSecondary))
325 } 324 }
326 } 325 }
327 } 326 }
@@ -337,11 +336,11 @@ struct AuditSessionDetailView: View {
337 Spacer() 336 Spacer()
338 Text(point.status.title) 337 Text(point.status.title)
339 .font(appDensity.font(.caption2)) 338 .font(appDensity.font(.caption2))
340 .foregroundStyle(.secondary) 339 .foregroundStyle(Color(.appTextSecondary))
341 } 340 }
342 Text("Findings \(point.findingCount) • Open high \(point.openHighSeverityCount) • Repeated \(point.repeatedIssueCount)") 341 Text("Findings \(point.findingCount) • Open high \(point.openHighSeverityCount) • Repeated \(point.repeatedIssueCount)")
343 .font(appDensity.font(.caption2)) 342 .font(appDensity.font(.caption2))
344 .foregroundStyle(.secondary) 343 .foregroundStyle(Color(.appTextSecondary))
345 } 344 }
346 } 345 }
347 } 346 }
@@ -401,7 +400,6 @@ struct AuditSessionDetailView: View {
401 .background(Color(.appBackground)) 400 .background(Color(.appBackground))
402 } 401 }
403 } 402 }
404 .preferredColorScheme(.dark)
405 } 403 }
406} 404}
407 405
@@ -459,7 +457,7 @@ private struct AuditFindingEditorView: View {
459 ForEach(session.evidence.screenshots) { asset in 457 ForEach(session.evidence.screenshots) { asset in
460 Text("\(asset.title): \(asset.reference)") 458 Text("\(asset.title): \(asset.reference)")
461 .font(.caption) 459 .font(.caption)
462 .foregroundStyle(.secondary) 460 .foregroundStyle(Color(.appTextSecondary))
463 } 461 }
464 } 462 }
465 } 463 }
DomainDig/DomainDigApp.swift +3
@@ -12,6 +12,7 @@ struct DomainDigApp: App {
12 @UIApplicationDelegateAdaptor(DomainDigAppDelegate.self) private var appDelegate 12 @UIApplicationDelegateAdaptor(DomainDigAppDelegate.self) private var appDelegate
13 @Environment(\.scenePhase) private var scenePhase 13 @Environment(\.scenePhase) private var scenePhase
14 @AppStorage(AppDensity.userDefaultsKey) private var density = AppDensity.compact.rawValue 14 @AppStorage(AppDensity.userDefaultsKey) private var density = AppDensity.compact.rawValue
15 @AppStorage(AppAppearance.userDefaultsKey) private var appearance = AppAppearance.system.rawValue
15 @State private var viewModel = DomainViewModel() 16 @State private var viewModel = DomainViewModel()
16 @State private var purchaseService = PurchaseService.shared 17 @State private var purchaseService = PurchaseService.shared
17 @State private var cloudSyncService = CloudSyncService.shared 18 @State private var cloudSyncService = CloudSyncService.shared
@@ -27,6 +28,8 @@ struct DomainDigApp: App {
27 WindowGroup { 28 WindowGroup {
28 RootTabView(viewModel: viewModel) 29 RootTabView(viewModel: viewModel)
29 .environment(\.appDensity, AppDensity(rawValue: density) ?? .compact) 30 .environment(\.appDensity, AppDensity(rawValue: density) ?? .compact)
31 // The single place appearance is applied. Keep it that way.
32 .preferredColorScheme((AppAppearance(rawValue: appearance) ?? .system).colorScheme)
30 .task { 33 .task {
31 let _ = purchaseService.currentTier 34 let _ = purchaseService.currentTier
32 let _ = cloudSyncService.status 35 let _ = cloudSyncService.status
DomainDig/DomainDigUI.swift +47 −8
@@ -6,6 +6,45 @@ import UIKit
6import AppKit 6import AppKit
7#endif 7#endif
8 8
9/// User-selected appearance, applied once at the `WindowGroup`.
10///
11/// Deliberately applied in exactly one place. The app previously carried 16
12/// separate `.preferredColorScheme(.dark)` calls scattered across view bodies,
13/// which is how it became impossible to reach light mode at all — re-applying
14/// per view is what let the lock spread unnoticed.
15enum AppAppearance: String, CaseIterable, Identifiable {
16 case system
17 case light
18 case dark
19
20 static let userDefaultsKey = "appAppearance"
21
22 var id: String { rawValue }
23
24 var title: String {
25 switch self {
26 case .system:
27 return "System"
28 case .light:
29 return "Light"
30 case .dark:
31 return "Dark"
32 }
33 }
34
35 /// `nil` hands control back to the system setting.
36 var colorScheme: ColorScheme? {
37 switch self {
38 case .system:
39 return nil
40 case .light:
41 return .light
42 case .dark:
43 return .dark
44 }
45 }
46}
47
9enum AppDensity: String, CaseIterable, Identifiable { 48enum AppDensity: String, CaseIterable, Identifiable {
10 case compact 49 case compact
11 case comfortable 50 case comfortable
@@ -96,7 +135,7 @@ enum AppStatusFactory {
96 case .registered: 135 case .registered:
97 return .init(title: "Registered", systemImage: "circle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) 136 return .init(title: "Registered", systemImage: "circle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
98 case .unknown, .none: 137 case .unknown, .none:
99 return .init(title: "Unknown", systemImage: "questionmark.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) 138 return .init(title: "Unknown", systemImage: "questionmark.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
100 } 139 }
101 } 140 }
102 141
@@ -112,7 +151,7 @@ enum AppStatusFactory {
112 151
113 static func email(_ result: EmailSecurityResult?, error: String?) -> AppStatusBadgeModel { 152 static func email(_ result: EmailSecurityResult?, error: String?) -> AppStatusBadgeModel {
114 guard error == nil, let result else { 153 guard error == nil, let result else {
115 return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) 154 return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
116 } 155 }
117 156
118 let foundCount = [result.spf.found, result.dmarc.found, result.dkim.found].filter { $0 }.count 157 let foundCount = [result.spf.found, result.dmarc.found, result.dkim.found].filter { $0 }.count
@@ -122,18 +161,18 @@ enum AppStatusFactory {
122 case 1, 2: 161 case 1, 2:
123 return .init(title: "Partial", systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16)) 162 return .init(title: "Partial", systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
124 default: 163 default:
125 return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) 164 return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
126 } 165 }
127 } 166 }
128 167
129 static func change(_ summary: DomainChangeSummary?) -> AppStatusBadgeModel { 168 static func change(_ summary: DomainChangeSummary?) -> AppStatusBadgeModel {
130 guard let summary else { 169 guard let summary else {
131 return .init(title: "Unchanged", systemImage: "circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) 170 return .init(title: "Unchanged", systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
132 } 171 }
133 if summary.hasChanges { 172 if summary.hasChanges {
134 return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16)) 173 return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16))
135 } 174 }
136 return .init(title: "Unchanged", systemImage: "checkmark.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)) 175 return .init(title: "Unchanged", systemImage: "checkmark.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
137 } 176 }
138} 177}
139 178
@@ -259,7 +298,7 @@ struct EmptyStateCardView: View {
259 298
260 Text(message) 299 Text(message)
261 .font(appDensity.font(.body)) 300 .font(appDensity.font(.body))
262 .foregroundStyle(.secondary) 301 .foregroundStyle(Color(.appTextSecondary))
263 .fixedSize(horizontal: false, vertical: true) 302 .fixedSize(horizontal: false, vertical: true)
264 303
265 Text(suggestion) 304 Text(suggestion)
@@ -311,14 +350,14 @@ struct CollapsibleSectionView<HeaderTrailing: View, Content: View>: View {
311 if let subtitle { 350 if let subtitle {
312 Text(subtitle) 351 Text(subtitle)
313 .font(appDensity.font(.caption)) 352 .font(appDensity.font(.caption))
314 .foregroundStyle(.secondary) 353 .foregroundStyle(Color(.appTextSecondary))
315 } 354 }
316 } 355 }
317 Spacer(minLength: 8) 356 Spacer(minLength: 8)
318 trailing() 357 trailing()
319 Image(systemName: isCollapsed ? "chevron.down" : "chevron.up") 358 Image(systemName: isCollapsed ? "chevron.down" : "chevron.up")
320 .font(.caption.weight(.semibold)) 359 .font(.caption.weight(.semibold))
321 .foregroundStyle(.secondary) 360 .foregroundStyle(Color(.appTextSecondary))
322 } 361 }
323 .contentShape(Rectangle()) 362 .contentShape(Rectangle())
324 .frame(minHeight: appDensity.metrics.controlMinHeight, alignment: .center) 363 .frame(minHeight: appDensity.metrics.controlMinHeight, alignment: .center)
DomainDig/HistoryView.swift +5 −7
@@ -40,12 +40,12 @@ struct HistoryView: View {
40 Spacer() 40 Spacer()
41 Text("\(item.count) snapshots") 41 Text("\(item.count) snapshots")
42 .font(appDensity.font(.caption2)) 42 .font(appDensity.font(.caption2))
43 .foregroundStyle(.secondary) 43 .foregroundStyle(Color(.appTextSecondary))
44 } 44 }
45 45
46 Text(item.latest.changeSummaryMessage ?? "No change summary") 46 Text(item.latest.changeSummaryMessage ?? "No change summary")
47 .font(appDensity.font(.caption)) 47 .font(appDensity.font(.caption))
48 .foregroundStyle(.secondary) 48 .foregroundStyle(Color(.appTextSecondary))
49 .lineLimit(2) 49 .lineLimit(2)
50 50
51 HStack(spacing: 8) { 51 HStack(spacing: 8) {
@@ -129,7 +129,6 @@ struct HistoryView: View {
129 .onChange(of: viewModel.rerunNavigationToken) { _, _ in 129 .onChange(of: viewModel.rerunNavigationToken) { _, _ in
130 dismiss() 130 dismiss()
131 } 131 }
132 .preferredColorScheme(.dark)
133 } 132 }
134 133
135 private func deleteFilteredHistoryEntries(at offsets: IndexSet) { 134 private func deleteFilteredHistoryEntries(at offsets: IndexSet) {
@@ -372,7 +371,6 @@ struct HistoryDetailView: View {
372 } 371 }
373 } 372 }
374 } 373 }
375 .preferredColorScheme(.dark)
376 } 374 }
377 375
378 private var snapshotBanner: some View { 376 private var snapshotBanner: some View {
@@ -385,7 +383,7 @@ struct HistoryDetailView: View {
385 Spacer() 383 Spacer()
386 Text("Live re-run available") 384 Text("Live re-run available")
387 .font(appDensity.font(.caption2)) 385 .font(appDensity.font(.caption2))
388 .foregroundStyle(.secondary) 386 .foregroundStyle(Color(.appTextSecondary))
389 } 387 }
390 if let mismatchNote = viewModel.resolverMismatchNote(for: entry) { 388 if let mismatchNote = viewModel.resolverMismatchNote(for: entry) {
391 Text(mismatchNote) 389 Text(mismatchNote)
@@ -400,10 +398,10 @@ struct HistoryDetailView: View {
400 if let note = entry.note, !note.isEmpty { 398 if let note = entry.note, !note.isEmpty {
401 Text(note) 399 Text(note)
402 .font(appDensity.font(.caption2)) 400 .font(appDensity.font(.caption2))
403 .foregroundStyle(.secondary) 401 .foregroundStyle(Color(.appTextSecondary))
404 } 402 }
405 } 403 }
406 .foregroundStyle(.secondary) 404 .foregroundStyle(Color(.appTextSecondary))
407 .padding(8) 405 .padding(8)
408 .frame(maxWidth: .infinity, alignment: .leading) 406 .frame(maxWidth: .infinity, alignment: .leading)
409 .background(Color(.appSurface)) 407 .background(Color(.appSurface))
DomainDig/IntegrationsView.swift +11 −11
@@ -15,7 +15,7 @@ struct IntegrationsSettingsView: View {
15 if let statusMessage = integrationService.statusMessage { 15 if let statusMessage = integrationService.statusMessage {
16 Text(statusMessage) 16 Text(statusMessage)
17 .font(.caption) 17 .font(.caption)
18 .foregroundStyle(.secondary) 18 .foregroundStyle(Color(.appTextSecondary))
19 } 19 }
20 20
21 Button("Process Queue Now") { 21 Button("Process Queue Now") {
@@ -26,7 +26,7 @@ struct IntegrationsSettingsView: View {
26 Section("Targets") { 26 Section("Targets") {
27 if integrationService.targets.isEmpty { 27 if integrationService.targets.isEmpty {
28 Text("No integrations configured.") 28 Text("No integrations configured.")
29 .foregroundStyle(.secondary) 29 .foregroundStyle(Color(.appTextSecondary))
30 } else { 30 } else {
31 ForEach(integrationService.targets) { target in 31 ForEach(integrationService.targets) { target in
32 NavigationLink { 32 NavigationLink {
@@ -42,12 +42,12 @@ struct IntegrationsSettingsView: View {
42 Text(target.name) 42 Text(target.name)
43 Spacer() 43 Spacer()
44 Text(target.type.title) 44 Text(target.type.title)
45 .foregroundStyle(.secondary) 45 .foregroundStyle(Color(.appTextSecondary))
46 } 46 }
47 47
48 Text(summary(for: target)) 48 Text(summary(for: target))
49 .font(.caption) 49 .font(.caption)
50 .foregroundStyle(.secondary) 50 .foregroundStyle(Color(.appTextSecondary))
51 51
52 if !target.isEnabled { 52 if !target.isEnabled {
53 Text("Disabled") 53 Text("Disabled")
@@ -138,7 +138,7 @@ private struct IntegrationDetailView: View {
138 Section("Delivery Log") { 138 Section("Delivery Log") {
139 if integrationService.deliveryRecords(for: target.id).isEmpty { 139 if integrationService.deliveryRecords(for: target.id).isEmpty {
140 Text("No deliveries yet.") 140 Text("No deliveries yet.")
141 .foregroundStyle(.secondary) 141 .foregroundStyle(Color(.appTextSecondary))
142 } else { 142 } else {
143 ForEach(integrationService.deliveryRecords(for: target.id), id: \.id) { record in 143 ForEach(integrationService.deliveryRecords(for: target.id), id: \.id) { record in
144 VStack(alignment: .leading, spacing: 4) { 144 VStack(alignment: .leading, spacing: 4) {
@@ -147,7 +147,7 @@ private struct IntegrationDetailView: View {
147 Spacer() 147 Spacer()
148 Text(record.timestamp.formatted(date: .abbreviated, time: .shortened)) 148 Text(record.timestamp.formatted(date: .abbreviated, time: .shortened))
149 .font(.caption) 149 .font(.caption)
150 .foregroundStyle(.secondary) 150 .foregroundStyle(Color(.appTextSecondary))
151 } 151 }
152 152
153 Text(record.summary) 153 Text(record.summary)
@@ -155,7 +155,7 @@ private struct IntegrationDetailView: View {
155 155
156 Text(record.destination) 156 Text(record.destination)
157 .font(.caption) 157 .font(.caption)
158 .foregroundStyle(.secondary) 158 .foregroundStyle(Color(.appTextSecondary))
159 159
160 if let failureReason = record.failureReason { 160 if let failureReason = record.failureReason {
161 let failureColor: Color = record.status == .skipped ? .secondary : Color(.statusCritical) 161 let failureColor: Color = record.status == .skipped ? .secondary : Color(.statusCritical)
@@ -169,7 +169,7 @@ private struct IntegrationDetailView: View {
169 } 169 }
170 } else { 170 } else {
171 Text("Integration not found.") 171 Text("Integration not found.")
172 .foregroundStyle(.secondary) 172 .foregroundStyle(Color(.appTextSecondary))
173 } 173 }
174 } 174 }
175 .navigationTitle(target?.name ?? "Integration") 175 .navigationTitle(target?.name ?? "Integration")
@@ -265,7 +265,7 @@ private struct IntegrationEditorView: View {
265 if existingTarget != nil { 265 if existingTarget != nil {
266 Text("Saved webhook URL remains in Keychain unless you replace it.") 266 Text("Saved webhook URL remains in Keychain unless you replace it.")
267 .font(.caption) 267 .font(.caption)
268 .foregroundStyle(.secondary) 268 .foregroundStyle(Color(.appTextSecondary))
269 } 269 }
270 } 270 }
271 case .slack: 271 case .slack:
@@ -278,7 +278,7 @@ private struct IntegrationEditorView: View {
278 if existingTarget != nil { 278 if existingTarget != nil {
279 Text("Saved Slack webhook remains in Keychain unless you replace it.") 279 Text("Saved Slack webhook remains in Keychain unless you replace it.")
280 .font(.caption) 280 .font(.caption)
281 .foregroundStyle(.secondary) 281 .foregroundStyle(Color(.appTextSecondary))
282 } 282 }
283 } 283 }
284 case .email: 284 case .email:
@@ -315,7 +315,7 @@ private struct IntegrationEditorView: View {
315 if existingTarget != nil { 315 if existingTarget != nil {
316 Text("Saved SMTP password remains in Keychain unless you replace it.") 316 Text("Saved SMTP password remains in Keychain unless you replace it.")
317 .font(.caption) 317 .font(.caption)
318 .foregroundStyle(.secondary) 318 .foregroundStyle(Color(.appTextSecondary))
319 } 319 }
320 } 320 }
321 } 321 }
DomainDig/MonitoringView.swift +6 −8
@@ -31,7 +31,7 @@ struct MonitoringView: View {
31 !monitoringStatusMessage.isEmpty { 31 !monitoringStatusMessage.isEmpty {
32 Text(monitoringStatusMessage) 32 Text(monitoringStatusMessage)
33 .font(appDensity.font(.caption)) 33 .font(appDensity.font(.caption))
34 .foregroundStyle(.secondary) 34 .foregroundStyle(Color(.appTextSecondary))
35 } 35 }
36 36
37 Button(viewModel.monitoringRunInProgress ? "Monitoring…" : "Run Now") { 37 Button(viewModel.monitoringRunInProgress ? "Monitoring…" : "Run Now") {
@@ -68,12 +68,12 @@ struct MonitoringView: View {
68 Spacer() 68 Spacer()
69 Text(log.timestamp.formatted(date: .abbreviated, time: .shortened)) 69 Text(log.timestamp.formatted(date: .abbreviated, time: .shortened))
70 .font(appDensity.font(.caption2)) 70 .font(appDensity.font(.caption2))
71 .foregroundStyle(.secondary) 71 .foregroundStyle(Color(.appTextSecondary))
72 } 72 }
73 73
74 Text(log.summary) 74 Text(log.summary)
75 .font(appDensity.font(.caption)) 75 .font(appDensity.font(.caption))
76 .foregroundStyle(.secondary) 76 .foregroundStyle(Color(.appTextSecondary))
77 77
78 HStack(spacing: 8) { 78 HStack(spacing: 8) {
79 metricBadge(title: "\(log.domainsChecked) checked") 79 metricBadge(title: "\(log.domainsChecked) checked")
@@ -95,7 +95,6 @@ struct MonitoringView: View {
95 .scrollContentBackground(.hidden) 95 .scrollContentBackground(.hidden)
96 .background(Color(.appBackground)) 96 .background(Color(.appBackground))
97 .navigationTitle("Monitoring") 97 .navigationTitle("Monitoring")
98 .preferredColorScheme(.dark)
99 .onAppear { 98 .onAppear {
100 viewModel.refreshMonitoringState() 99 viewModel.refreshMonitoringState()
101 } 100 }
@@ -152,7 +151,7 @@ struct MonitoringLogDetailView: View {
152 151
153 Text(result.summaryMessage) 152 Text(result.summaryMessage)
154 .font(appDensity.font(.caption)) 153 .font(appDensity.font(.caption))
155 .foregroundStyle(.secondary) 154 .foregroundStyle(Color(.appTextSecondary))
156 155
157 HStack(spacing: 10) { 156 HStack(spacing: 10) {
158 Text(result.resultSource.label) 157 Text(result.resultSource.label)
@@ -162,7 +161,7 @@ struct MonitoringLogDetailView: View {
162 } 161 }
163 } 162 }
164 .font(appDensity.font(.caption2)) 163 .font(appDensity.font(.caption2))
165 .foregroundStyle(.secondary) 164 .foregroundStyle(Color(.appTextSecondary))
166 165
167 if let errorMessage = result.errorMessage { 166 if let errorMessage = result.errorMessage {
168 Text(errorMessage) 167 Text(errorMessage)
@@ -188,7 +187,7 @@ struct MonitoringLogDetailView: View {
188 ForEach(log.errors, id: \.self) { error in 187 ForEach(log.errors, id: \.self) { error in
189 Text(error) 188 Text(error)
190 .font(appDensity.font(.caption)) 189 .font(appDensity.font(.caption))
191 .foregroundStyle(.secondary) 190 .foregroundStyle(Color(.appTextSecondary))
192 } 191 }
193 } 192 }
194 .listRowBackground(Color(.appSurface)) 193 .listRowBackground(Color(.appSurface))
@@ -197,7 +196,6 @@ struct MonitoringLogDetailView: View {
197 .scrollContentBackground(.hidden) 196 .scrollContentBackground(.hidden)
198 .background(Color(.appBackground)) 197 .background(Color(.appBackground))
199 .navigationTitle("Run Details") 198 .navigationTitle("Run Details")
200 .preferredColorScheme(.dark)
201 } 199 }
202 200
203 private func color(for severity: MonitoringAlertSeverity) -> Color { 201 private func color(for severity: MonitoringAlertSeverity) -> Color {
DomainDig/PaywallView.swift +4 −4
@@ -12,7 +12,7 @@ struct PaywallView: View {
12 Section { 12 Section {
13 Text("Pro unlocks workflows, scale, monitoring automation, and exports. Pro+ adds deeper external intelligence with no additional usage limits.") 13 Text("Pro unlocks workflows, scale, monitoring automation, and exports. Pro+ adds deeper external intelligence with no additional usage limits.")
14 .font(appDensity.font(.body, design: .default)) 14 .font(appDensity.font(.body, design: .default))
15 .foregroundStyle(.secondary) 15 .foregroundStyle(Color(.appTextSecondary))
16 .fixedSize(horizontal: false, vertical: true) 16 .fixedSize(horizontal: false, vertical: true)
17 } 17 }
18 18
@@ -38,7 +38,7 @@ struct PaywallView: View {
38 } else if purchaseService.products.isEmpty { 38 } else if purchaseService.products.isEmpty {
39 Text("Pricing is unavailable right now.") 39 Text("Pricing is unavailable right now.")
40 .font(appDensity.font(.caption, design: .default)) 40 .font(appDensity.font(.caption, design: .default))
41 .foregroundStyle(.secondary) 41 .foregroundStyle(Color(.appTextSecondary))
42 42
43 Button("Retry") { 43 Button("Retry") {
44 Task { 44 Task {
@@ -58,7 +58,7 @@ struct PaywallView: View {
58 .foregroundStyle(.primary) 58 .foregroundStyle(.primary)
59 Text(product.displayPrice) 59 Text(product.displayPrice)
60 .font(appDensity.font(.callout, design: .default)) 60 .font(appDensity.font(.callout, design: .default))
61 .foregroundStyle(.secondary) 61 .foregroundStyle(Color(.appTextSecondary))
62 } 62 }
63 .frame(maxWidth: .infinity, alignment: .leading) 63 .frame(maxWidth: .infinity, alignment: .leading)
64 } 64 }
@@ -69,7 +69,7 @@ struct PaywallView: View {
69 if let statusMessage = purchaseService.statusMessage { 69 if let statusMessage = purchaseService.statusMessage {
70 Text(statusMessage) 70 Text(statusMessage)
71 .font(appDensity.font(.caption, design: .default)) 71 .font(appDensity.font(.caption, design: .default))
72 .foregroundStyle(.secondary) 72 .foregroundStyle(Color(.appTextSecondary))
73 } 73 }
74 74
75 if let errorMessage = purchaseService.errorMessage { 75 if let errorMessage = purchaseService.errorMessage {
DomainDig/SavedDomainsView.swift +1 −2
@@ -9,7 +9,7 @@ struct SavedDomainsView: View {
9 if viewModel.savedDomains.isEmpty { 9 if viewModel.savedDomains.isEmpty {
10 Text("No saved domains") 10 Text("No saved domains")
11 .font(.system(.callout, design: .monospaced)) 11 .font(.system(.callout, design: .monospaced))
12 .foregroundStyle(.secondary) 12 .foregroundStyle(Color(.appTextSecondary))
13 .listRowBackground(Color(.appSurface)) 13 .listRowBackground(Color(.appSurface))
14 } else { 14 } else {
15 ForEach(viewModel.savedDomains, id: \.self) { domain in 15 ForEach(viewModel.savedDomains, id: \.self) { domain in
@@ -37,6 +37,5 @@ struct SavedDomainsView: View {
37 EditButton() 37 EditButton()
38 } 38 }
39 } 39 }
40 .preferredColorScheme(.dark)
41 } 40 }
42} 41}
DomainDig/ScheduledReportsView.swift +3 −3
@@ -13,7 +13,7 @@ struct ScheduledReportsView: View {
13 if !FeatureAccessService.hasAccess(to: .automatedMonitoring) { 13 if !FeatureAccessService.hasAccess(to: .automatedMonitoring) {
14 Text("Scheduled reports require Pro.") 14 Text("Scheduled reports require Pro.")
15 .font(appDensity.font(.caption)) 15 .font(appDensity.font(.caption))
16 .foregroundStyle(.secondary) 16 .foregroundStyle(Color(.appTextSecondary))
17 } 17 }
18 18
19 Toggle("Scheduled Reports", isOn: Binding( 19 Toggle("Scheduled Reports", isOn: Binding(
@@ -59,7 +59,7 @@ struct ScheduledReportsView: View {
59 if let statusMessage { 59 if let statusMessage {
60 Text(statusMessage) 60 Text(statusMessage)
61 .font(appDensity.font(.caption)) 61 .font(appDensity.font(.caption))
62 .foregroundStyle(.secondary) 62 .foregroundStyle(Color(.appTextSecondary))
63 } 63 }
64 64
65 Button(isGenerating ? "Generating…" : "Generate Now") { 65 Button(isGenerating ? "Generating…" : "Generate Now") {
@@ -91,7 +91,7 @@ struct ScheduledReportsView: View {
91 .foregroundStyle(.primary) 91 .foregroundStyle(.primary)
92 Text(log.generatedAt.formatted(date: .abbreviated, time: .shortened)) 92 Text(log.generatedAt.formatted(date: .abbreviated, time: .shortened))
93 .font(.caption) 93 .font(.caption)
94 .foregroundStyle(.secondary) 94 .foregroundStyle(Color(.appTextSecondary))
95 } 95 }
96 } 96 }
97 } 97 }
DomainDig/TimelineView.swift +4 −4
@@ -125,7 +125,7 @@ private struct TimelineRow: View {
125 125
126 Text(summary.changeSummaryMessage ?? "No change summary") 126 Text(summary.changeSummaryMessage ?? "No change summary")
127 .font(appDensity.font(.caption)) 127 .font(appDensity.font(.caption))
128 .foregroundStyle(.secondary) 128 .foregroundStyle(Color(.appTextSecondary))
129 .lineLimit(2) 129 .lineLimit(2)
130 130
131 HStack(spacing: 8) { 131 HStack(spacing: 8) {
@@ -137,14 +137,14 @@ private struct TimelineRow: View {
137 } 137 }
138 } 138 }
139 .font(appDensity.font(.caption2)) 139 .font(appDensity.font(.caption2))
140 .foregroundStyle(.secondary) 140 .foregroundStyle(Color(.appTextSecondary))
141 141
142 if !entry.intelligenceTimeline.isEmpty { 142 if !entry.intelligenceTimeline.isEmpty {
143 VStack(alignment: .leading, spacing: 4) { 143 VStack(alignment: .leading, spacing: 4) {
144 ForEach(Array(entry.intelligenceTimeline.prefix(2))) { event in 144 ForEach(Array(entry.intelligenceTimeline.prefix(2))) { event in
145 Text("\(event.title): \(event.detail)") 145 Text("\(event.title): \(event.detail)")
146 .font(appDensity.font(.caption2)) 146 .font(appDensity.font(.caption2))
147 .foregroundStyle(.secondary) 147 .foregroundStyle(Color(.appTextSecondary))
148 .lineLimit(1) 148 .lineLimit(1)
149 } 149 }
150 } 150 }
@@ -161,7 +161,7 @@ private struct TimelineRow: View {
161 .lineLimit(1) 161 .lineLimit(1)
162 } 162 }
163 .font(appDensity.font(.caption2)) 163 .font(appDensity.font(.caption2))
164 .foregroundStyle(.secondary) 164 .foregroundStyle(Color(.appTextSecondary))
165 } 165 }
166 } 166 }
167} 167}
DomainDig/WatchlistView.swift +12 −14
@@ -42,7 +42,7 @@ struct WatchlistView: View {
42 HStack { 42 HStack {
43 Text(viewModel.batchProgressLabel) 43 Text(viewModel.batchProgressLabel)
44 .font(appDensity.font(.caption)) 44 .font(appDensity.font(.caption))
45 .foregroundStyle(.secondary) 45 .foregroundStyle(Color(.appTextSecondary))
46 Spacer() 46 Spacer()
47 if viewModel.batchLookupRunning { 47 if viewModel.batchLookupRunning {
48 Button("Cancel") { 48 Button("Cancel") {
@@ -78,7 +78,7 @@ struct WatchlistView: View {
78 Section { 78 Section {
79 Text(limitMessage) 79 Text(limitMessage)
80 .font(appDensity.font(.caption)) 80 .font(appDensity.font(.caption))
81 .foregroundStyle(.secondary) 81 .foregroundStyle(Color(.appTextSecondary))
82 } 82 }
83 .listRowBackground(Color(.appSurface)) 83 .listRowBackground(Color(.appSurface))
84 } 84 }
@@ -208,7 +208,7 @@ struct WatchlistView: View {
208 Section { 208 Section {
209 Text("Adds the domain directly to your watchlist so monitoring can run without a prior inspection.") 209 Text("Adds the domain directly to your watchlist so monitoring can run without a prior inspection.")
210 .font(appDensity.font(.caption)) 210 .font(appDensity.font(.caption))
211 .foregroundStyle(.secondary) 211 .foregroundStyle(Color(.appTextSecondary))
212 } 212 }
213 213
214 if let addDomainError { 214 if let addDomainError {
@@ -267,7 +267,7 @@ struct WatchlistView: View {
267 .foregroundStyle(.primary) 267 .foregroundStyle(.primary)
268 Text([view.tag, view.filter.title, view.sort.title].compactMap { $0 }.joined(separator: " • ")) 268 Text([view.tag, view.filter.title, view.sort.title].compactMap { $0 }.joined(separator: " • "))
269 .font(.caption) 269 .font(.caption)
270 .foregroundStyle(.secondary) 270 .foregroundStyle(Color(.appTextSecondary))
271 } 271 }
272 } 272 }
273 } 273 }
@@ -288,7 +288,6 @@ struct WatchlistView: View {
288 } 288 }
289 } 289 }
290 } 290 }
291 .preferredColorScheme(.dark)
292 } 291 }
293 292
294 @ViewBuilder 293 @ViewBuilder
@@ -450,12 +449,12 @@ struct WatchlistRowView: View {
450 449
451 Text("Updated \(trackedDomain.updatedAt.formatted(date: .abbreviated, time: .shortened))") 450 Text("Updated \(trackedDomain.updatedAt.formatted(date: .abbreviated, time: .shortened))")
452 .font(appDensity.font(.caption2)) 451 .font(appDensity.font(.caption2))
453 .foregroundStyle(.secondary) 452 .foregroundStyle(Color(.appTextSecondary))
454 453
455 if let collaboration = trackedDomain.collaboration, collaboration.isShared { 454 if let collaboration = trackedDomain.collaboration, collaboration.isShared {
456 Text("\(collaboration.ownership.title) • \(collaboration.permission.title)") 455 Text("\(collaboration.ownership.title) • \(collaboration.permission.title)")
457 .font(appDensity.font(.caption2)) 456 .font(appDensity.font(.caption2))
458 .foregroundStyle(.secondary) 457 .foregroundStyle(Color(.appTextSecondary))
459 } 458 }
460 459
461 HStack(spacing: 8) { 460 HStack(spacing: 8) {
@@ -468,19 +467,19 @@ struct WatchlistRowView: View {
468 } 467 }
469 } 468 }
470 .font(appDensity.font(.caption2)) 469 .font(appDensity.font(.caption2))
471 .foregroundStyle(.secondary) 470 .foregroundStyle(Color(.appTextSecondary))
472 471
473 indicatorRow 472 indicatorRow
474 473
475 if let note = trackedDomain.note?.trimmingCharacters(in: .whitespacesAndNewlines), !note.isEmpty { 474 if let note = trackedDomain.note?.trimmingCharacters(in: .whitespacesAndNewlines), !note.isEmpty {
476 Text(note) 475 Text(note)
477 .font(appDensity.font(.caption)) 476 .font(appDensity.font(.caption))
478 .foregroundStyle(.secondary) 477 .foregroundStyle(Color(.appTextSecondary))
479 .lineLimit(2) 478 .lineLimit(2)
480 } else if let summary = trackedDomain.lastChangeSummary { 479 } else if let summary = trackedDomain.lastChangeSummary {
481 Text(summary.message) 480 Text(summary.message)
482 .font(appDensity.font(.caption)) 481 .font(appDensity.font(.caption))
483 .foregroundStyle(.secondary) 482 .foregroundStyle(Color(.appTextSecondary))
484 .lineLimit(2) 483 .lineLimit(2)
485 } 484 }
486 } 485 }
@@ -502,7 +501,7 @@ struct WatchlistRowView: View {
502 @ViewBuilder 501 @ViewBuilder
503 private var statusBadge: some View { 502 private var statusBadge: some View {
504 if isRefreshing { 503 if isRefreshing {
505 AppStatusBadgeView(model: .init(title: "Refreshing", systemImage: "arrow.clockwise", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated))) 504 AppStatusBadgeView(model: .init(title: "Refreshing", systemImage: "arrow.clockwise", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated)))
506 } else { 505 } else {
507 AppStatusBadgeView(model: AppStatusFactory.availability(trackedDomain.lastKnownAvailability)) 506 AppStatusBadgeView(model: AppStatusFactory.availability(trackedDomain.lastKnownAvailability))
508 } 507 }
@@ -700,7 +699,7 @@ struct TrackedDomainDetailView: View {
700 if latestSnapshots.isEmpty { 699 if latestSnapshots.isEmpty {
701 Text("No snapshots yet") 700 Text("No snapshots yet")
702 .font(.system(.caption, design: .monospaced)) 701 .font(.system(.caption, design: .monospaced))
703 .foregroundStyle(.secondary) 702 .foregroundStyle(Color(.appTextSecondary))
704 } else { 703 } else {
705 ForEach(latestSnapshots) { entry in 704 ForEach(latestSnapshots) { entry in
706 NavigationLink { 705 NavigationLink {
@@ -712,7 +711,7 @@ struct TrackedDomainDetailView: View {
712 .foregroundStyle(.primary) 711 .foregroundStyle(.primary)
713 Text(entry.changeSummary?.hasChanges == true ? "Changed" : "Snapshot") 712 Text(entry.changeSummary?.hasChanges == true ? "Changed" : "Snapshot")
714 .font(.system(.caption2, design: .monospaced)) 713 .font(.system(.caption2, design: .monospaced))
715 .foregroundStyle(.secondary) 714 .foregroundStyle(Color(.appTextSecondary))
716 } 715 }
717 } 716 }
718 } 717 }
@@ -723,7 +722,6 @@ struct TrackedDomainDetailView: View {
723 .scrollContentBackground(.hidden) 722 .scrollContentBackground(.hidden)
724 .background(Color(.appBackground)) 723 .background(Color(.appBackground))
725 .navigationTitle(liveTrackedDomain.domain) 724 .navigationTitle(liveTrackedDomain.domain)
726 .preferredColorScheme(.dark)
727 .onChange(of: viewModel.rerunNavigationToken) { _, _ in 725 .onChange(of: viewModel.rerunNavigationToken) { _, _ in
728 dismiss() 726 dismiss()
729 } 727 }
DomainDig/WorkflowsView.swift +16 −21
@@ -22,7 +22,7 @@ struct WorkflowsView: View {
22 Section { 22 Section {
23 Text(limitMessage) 23 Text(limitMessage)
24 .font(appDensity.font(.caption)) 24 .font(appDensity.font(.caption))
25 .foregroundStyle(.secondary) 25 .foregroundStyle(Color(.appTextSecondary))
26 } 26 }
27 .listRowBackground(Color(.appSurface)) 27 .listRowBackground(Color(.appSurface))
28 } 28 }
@@ -54,7 +54,6 @@ struct WorkflowsView: View {
54 .sheet(item: workflowSummaryBinding) { summary in 54 .sheet(item: workflowSummaryBinding) { summary in
55 WorkflowRunSummaryView(viewModel: viewModel, summary: summary) 55 WorkflowRunSummaryView(viewModel: viewModel, summary: summary)
56 } 56 }
57 .preferredColorScheme(.dark)
58 } 57 }
59 58
60 private var workflowSummaryBinding: Binding<WorkflowRunSummary?> { 59 private var workflowSummaryBinding: Binding<WorkflowRunSummary?> {
@@ -78,7 +77,7 @@ struct WorkflowsView: View {
78 HStack { 77 HStack {
79 Text(viewModel.batchProgressLabel) 78 Text(viewModel.batchProgressLabel)
80 .font(appDensity.font(.caption)) 79 .font(appDensity.font(.caption))
81 .foregroundStyle(.secondary) 80 .foregroundStyle(Color(.appTextSecondary))
82 Spacer() 81 Spacer()
83 if viewModel.batchLookupRunning { 82 if viewModel.batchLookupRunning {
84 Button("Cancel") { 83 Button("Cancel") {
@@ -142,23 +141,23 @@ private struct WorkflowRowView: View {
142 Spacer(minLength: 8) 141 Spacer(minLength: 8)
143 Text("\(workflow.domains.count) domains") 142 Text("\(workflow.domains.count) domains")
144 .font(appDensity.font(.caption2)) 143 .font(appDensity.font(.caption2))
145 .foregroundStyle(.secondary) 144 .foregroundStyle(Color(.appTextSecondary))
146 } 145 }
147 146
148 Text("Updated \(workflow.updatedAt.formatted(date: .abbreviated, time: .shortened))") 147 Text("Updated \(workflow.updatedAt.formatted(date: .abbreviated, time: .shortened))")
149 .font(appDensity.font(.caption2)) 148 .font(appDensity.font(.caption2))
150 .foregroundStyle(.secondary) 149 .foregroundStyle(Color(.appTextSecondary))
151 150
152 if let collaboration = workflow.collaboration, collaboration.isShared { 151 if let collaboration = workflow.collaboration, collaboration.isShared {
153 Text("\(collaboration.ownership.title) • \(collaboration.permission.title)") 152 Text("\(collaboration.ownership.title) • \(collaboration.permission.title)")
154 .font(appDensity.font(.caption2)) 153 .font(appDensity.font(.caption2))
155 .foregroundStyle(.secondary) 154 .foregroundStyle(Color(.appTextSecondary))
156 } 155 }
157 156
158 if let notes = workflow.notes, !notes.isEmpty { 157 if let notes = workflow.notes, !notes.isEmpty {
159 Text(notes) 158 Text(notes)
160 .font(appDensity.font(.caption)) 159 .font(appDensity.font(.caption))
161 .foregroundStyle(.secondary) 160 .foregroundStyle(Color(.appTextSecondary))
162 .lineLimit(2) 161 .lineLimit(2)
163 } 162 }
164 } 163 }
@@ -197,12 +196,12 @@ struct WorkflowDetailView: View {
197 if let notes = workflow.notes, !notes.isEmpty { 196 if let notes = workflow.notes, !notes.isEmpty {
198 Text(notes) 197 Text(notes)
199 .font(appDensity.font(.caption)) 198 .font(appDensity.font(.caption))
200 .foregroundStyle(.secondary) 199 .foregroundStyle(Color(.appTextSecondary))
201 } 200 }
202 if let collaboration = workflow.collaboration, collaboration.isShared { 201 if let collaboration = workflow.collaboration, collaboration.isShared {
203 Text("\(collaboration.ownership.title) • \(collaboration.permission.title)") 202 Text("\(collaboration.ownership.title) • \(collaboration.permission.title)")
204 .font(appDensity.font(.caption)) 203 .font(appDensity.font(.caption))
205 .foregroundStyle(.secondary) 204 .foregroundStyle(Color(.appTextSecondary))
206 } 205 }
207 } 206 }
208 207
@@ -277,7 +276,7 @@ struct WorkflowDetailView: View {
277 if !latestSummary.workflowInsights.isEmpty { 276 if !latestSummary.workflowInsights.isEmpty {
278 Text(latestSummary.workflowInsights[0].description) 277 Text(latestSummary.workflowInsights[0].description)
279 .font(appDensity.font(.caption)) 278 .font(appDensity.font(.caption))
280 .foregroundStyle(.secondary) 279 .foregroundStyle(Color(.appTextSecondary))
281 } 280 }
282 281
283 Button { 282 Button {
@@ -306,7 +305,7 @@ struct WorkflowDetailView: View {
306 if workflow.domains.isEmpty { 305 if workflow.domains.isEmpty {
307 Text("No domains in this workflow") 306 Text("No domains in this workflow")
308 .font(appDensity.font(.caption)) 307 .font(appDensity.font(.caption))
309 .foregroundStyle(.secondary) 308 .foregroundStyle(Color(.appTextSecondary))
310 } else { 309 } else {
311 ForEach(workflow.domains, id: \.self) { domain in 310 ForEach(workflow.domains, id: \.self) { domain in
312 Button { 311 Button {
@@ -318,7 +317,7 @@ struct WorkflowDetailView: View {
318 .foregroundStyle(.primary) 317 .foregroundStyle(.primary)
319 Spacer() 318 Spacer()
320 Image(systemName: "arrow.up.right.circle") 319 Image(systemName: "arrow.up.right.circle")
321 .foregroundStyle(.secondary) 320 .foregroundStyle(Color(.appTextSecondary))
322 } 321 }
323 } 322 }
324 .buttonStyle(.plain) 323 .buttonStyle(.plain)
@@ -355,17 +354,16 @@ struct WorkflowDetailView: View {
355 } else { 354 } else {
356 Text("Workflow not found") 355 Text("Workflow not found")
357 .font(appDensity.font(.callout)) 356 .font(appDensity.font(.callout))
358 .foregroundStyle(.secondary) 357 .foregroundStyle(Color(.appTextSecondary))
359 } 358 }
360 } 359 }
361 .preferredColorScheme(.dark)
362 } 360 }
363 361
364 private func statRow(label: String, value: String) -> some View { 362 private func statRow(label: String, value: String) -> some View {
365 HStack { 363 HStack {
366 Text(label) 364 Text(label)
367 .font(appDensity.font(.caption)) 365 .font(appDensity.font(.caption))
368 .foregroundStyle(.secondary) 366 .foregroundStyle(Color(.appTextSecondary))
369 Spacer() 367 Spacer()
370 Text(value) 368 Text(value)
371 .font(appDensity.font(.callout)) 369 .font(appDensity.font(.callout))
@@ -449,7 +447,6 @@ struct WorkflowComposerView: View {
449 } 447 }
450 } 448 }
451 } 449 }
452 .preferredColorScheme(.dark)
453 } 450 }
454 451
455 private var parsedDomains: [String] { 452 private var parsedDomains: [String] {
@@ -507,7 +504,7 @@ struct WorkflowRunSummaryView: View {
507 .foregroundStyle(.primary) 504 .foregroundStyle(.primary)
508 Text(insight.domainsInvolved.joined(separator: ", ")) 505 Text(insight.domainsInvolved.joined(separator: ", "))
509 .font(.system(.caption, design: .monospaced)) 506 .font(.system(.caption, design: .monospaced))
510 .foregroundStyle(.secondary) 507 .foregroundStyle(Color(.appTextSecondary))
511 } 508 }
512 } 509 }
513 } 510 }
@@ -517,7 +514,7 @@ struct WorkflowRunSummaryView: View {
517 if visibleResults.isEmpty { 514 if visibleResults.isEmpty {
518 Text("No domains with meaningful changes or warnings") 515 Text("No domains with meaningful changes or warnings")
519 .font(.system(.caption, design: .monospaced)) 516 .font(.system(.caption, design: .monospaced))
520 .foregroundStyle(.secondary) 517 .foregroundStyle(Color(.appTextSecondary))
521 } else { 518 } else {
522 ForEach(visibleResults) { result in 519 ForEach(visibleResults) { result in
523 if let entry = viewModel.historyEntry(for: result) { 520 if let entry = viewModel.historyEntry(for: result) {
@@ -578,14 +575,13 @@ struct WorkflowRunSummaryView: View {
578 } 575 }
579 } 576 }
580 } 577 }
581 .preferredColorScheme(.dark)
582 } 578 }
583 579
584 private func statRow(label: String, value: String) -> some View { 580 private func statRow(label: String, value: String) -> some View {
585 HStack { 581 HStack {
586 Text(label) 582 Text(label)
587 .font(.system(.caption, design: .monospaced)) 583 .font(.system(.caption, design: .monospaced))
588 .foregroundStyle(.secondary) 584 .foregroundStyle(Color(.appTextSecondary))
589 Spacer() 585 Spacer()
590 Text(value) 586 Text(value)
591 .font(.system(.callout, design: .monospaced)) 587 .font(.system(.callout, design: .monospaced))
@@ -687,7 +683,6 @@ struct WorkflowBulkAddSheet: View {
687 } 683 }
688 } 684 }
689 } 685 }
690 .preferredColorScheme(.dark)
691 } 686 }
692 687
693 private var selectedDomainList: [String] { 688 private var selectedDomainList: [String] {
DomainDigWidget/DomainDigPortfolioWidget.swift +9 −9
@@ -61,10 +61,10 @@ struct DomainDigWidgetView: View {
61 VStack(spacing: 6) { 61 VStack(spacing: 6) {
62 Image(systemName: "magnifyingglass") 62 Image(systemName: "magnifyingglass")
63 .font(.title2) 63 .font(.title2)
64 .foregroundStyle(.secondary) 64 .foregroundStyle(Color(.appTextSecondary))
65 Text("No tracked domains") 65 Text("No tracked domains")
66 .font(.caption) 66 .font(.caption)
67 .foregroundStyle(.secondary) 67 .foregroundStyle(Color(.appTextSecondary))
68 .multilineTextAlignment(.center) 68 .multilineTextAlignment(.center)
69 } 69 }
70 } 70 }
@@ -80,13 +80,13 @@ struct DomainDigWidgetView: View {
80 Spacer() 80 Spacer()
81 } 81 }
82 .font(.caption2) 82 .font(.caption2)
83 .foregroundStyle(.secondary) 83 .foregroundStyle(Color(.appTextSecondary))
84 84
85 Text("\(data.totalDomains)") 85 Text("\(data.totalDomains)")
86 .font(.system(size: 34, weight: .bold, design: .rounded)) 86 .font(.system(size: 34, weight: .bold, design: .rounded))
87 Text("tracked") 87 Text("tracked")
88 .font(.caption2) 88 .font(.caption2)
89 .foregroundStyle(.secondary) 89 .foregroundStyle(Color(.appTextSecondary))
90 90
91 Spacer(minLength: 0) 91 Spacer(minLength: 0)
92 92
@@ -113,11 +113,11 @@ struct DomainDigWidgetView: View {
113 Label("Domain Portfolio", systemImage: "shield.lefthalf.filled") 113 Label("Domain Portfolio", systemImage: "shield.lefthalf.filled")
114 .font(.caption) 114 .font(.caption)
115 .fontWeight(.semibold) 115 .fontWeight(.semibold)
116 .foregroundStyle(.secondary) 116 .foregroundStyle(Color(.appTextSecondary))
117 Spacer() 117 Spacer()
118 Text("\(data.totalDomains) tracked") 118 Text("\(data.totalDomains) tracked")
119 .font(.caption2) 119 .font(.caption2)
120 .foregroundStyle(.secondary) 120 .foregroundStyle(Color(.appTextSecondary))
121 } 121 }
122 122
123 HStack(spacing: 12) { 123 HStack(spacing: 12) {
@@ -147,7 +147,7 @@ struct DomainDigWidgetView: View {
147 .foregroundStyle(color) 147 .foregroundStyle(color)
148 Text(label) 148 Text(label)
149 .font(.system(size: 9)) 149 .font(.system(size: 9))
150 .foregroundStyle(.secondary) 150 .foregroundStyle(Color(.appTextSecondary))
151 } 151 }
152 .frame(maxWidth: .infinity, alignment: .leading) 152 .frame(maxWidth: .infinity, alignment: .leading)
153 } 153 }
@@ -160,7 +160,7 @@ struct DomainDigWidgetView: View {
160 if domain.isPinned { 160 if domain.isPinned {
161 Image(systemName: "pin.fill") 161 Image(systemName: "pin.fill")
162 .font(.system(size: 8)) 162 .font(.system(size: 8))
163 .foregroundStyle(.secondary) 163 .foregroundStyle(Color(.appTextSecondary))
164 } 164 }
165 Text(domain.domain) 165 Text(domain.domain)
166 .font(.caption) 166 .font(.caption)
@@ -168,7 +168,7 @@ struct DomainDigWidgetView: View {
168 Spacer(minLength: 4) 168 Spacer(minLength: 4)
169 Text(certLabel(for: domain)) 169 Text(certLabel(for: domain))
170 .font(.caption2) 170 .font(.caption2)
171 .foregroundStyle(.secondary) 171 .foregroundStyle(Color(.appTextSecondary))
172 } 172 }
173 } 173 }
174 174
DomainDigWidget/SweepLiveActivity.swift +5 −5
@@ -14,7 +14,7 @@ struct SweepLiveActivity: Widget {
14 DynamicIslandExpandedRegion(.leading) { 14 DynamicIslandExpandedRegion(.leading) {
15 Label(context.attributes.title, systemImage: "arrow.trianglehead.2.clockwise") 15 Label(context.attributes.title, systemImage: "arrow.trianglehead.2.clockwise")
16 .font(.caption) 16 .font(.caption)
17 .foregroundStyle(.secondary) 17 .foregroundStyle(Color(.appTextSecondary))
18 } 18 }
19 DynamicIslandExpandedRegion(.trailing) { 19 DynamicIslandExpandedRegion(.trailing) {
20 Text("\(context.state.completed)/\(context.state.total)") 20 Text("\(context.state.completed)/\(context.state.total)")
@@ -28,7 +28,7 @@ struct SweepLiveActivity: Widget {
28 if let current = context.state.currentDomain { 28 if let current = context.state.currentDomain {
29 Text(current) 29 Text(current)
30 .font(.caption2) 30 .font(.caption2)
31 .foregroundStyle(.secondary) 31 .foregroundStyle(Color(.appTextSecondary))
32 .lineLimit(1) 32 .lineLimit(1)
33 } 33 }
34 } 34 }
@@ -58,7 +58,7 @@ private struct SweepActivityBannerView: View {
58 Label(context.attributes.title, systemImage: "arrow.trianglehead.2.clockwise") 58 Label(context.attributes.title, systemImage: "arrow.trianglehead.2.clockwise")
59 .font(.caption) 59 .font(.caption)
60 .fontWeight(.semibold) 60 .fontWeight(.semibold)
61 .foregroundStyle(.secondary) 61 .foregroundStyle(Color(.appTextSecondary))
62 Spacer() 62 Spacer()
63 Text("\(context.state.completed) of \(context.state.total)") 63 Text("\(context.state.completed) of \(context.state.total)")
64 .font(.caption) 64 .font(.caption)
@@ -72,12 +72,12 @@ private struct SweepActivityBannerView: View {
72 if let current = context.state.currentDomain { 72 if let current = context.state.currentDomain {
73 Text(current) 73 Text(current)
74 .font(.caption2) 74 .font(.caption2)
75 .foregroundStyle(.secondary) 75 .foregroundStyle(Color(.appTextSecondary))
76 .lineLimit(1) 76 .lineLimit(1)
77 } else { 77 } else {
78 Text(context.state.completed >= context.state.total ? "Finished" : "Working…") 78 Text(context.state.completed >= context.state.total ? "Finished" : "Working…")
79 .font(.caption2) 79 .font(.caption2)
80 .foregroundStyle(.secondary) 80 .foregroundStyle(Color(.appTextSecondary))
81 } 81 }
82 Spacer() 82 Spacer()
83 if context.state.changed > 0 { 83 if context.state.changed > 0 {
Shared/Colors.xcassets/AppTextSecondary.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": "0x5A",
9 "green": "0x5A",
10 "blue": "0x5F",
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": "0xA1",
27 "green": "0xA1",
28 "blue": "0xA6",
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": "0x3A",
45 "green": "0x3A",
46 "blue": "0x3C",
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": "0xC7",
67 "green": "0xC7",
68 "blue": "0xCC",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}