Commit eb8a0d30da

eb8a0d30da0cc150e95d44bad998c13ee2defc25

parent: 1e25fb947d

Unsigned

cmc <hello@cleberg.net> · 2026-07-20 23:16 UTC

feat(a11y): semantic colour system (#21 phase 1)

Replaces every hard-coded colour with semantic asset colours that adapt to
light, dark, and Increase Contrast. Dark mode stays locked, so this is a
pure refactor: the audit reports the same findings before and after.

The accent is now blue rather than cyan, per the tech/DNS theme.

Why custom values rather than the system palette: every system colour
fails WCAG AA in light mode. Measured on white — systemYellow 1.51:1,
systemOrange 2.20:1, systemGreen 2.22:1, systemCyan 2.54:1, systemRed
3.55:1. All of them pass in dark mode, which is why the dark-locked app
looked fine, and why unlocking light mode was never a matter of deleting
.preferredColorScheme(.dark). Every new value clears 4.5:1 as text on its
page, its card, and its own 16% badge tint — the way AppStatusBadgeView
actually draws it.

The accent needed splitting in two. As text on a dark background it must
be light; as a fill behind a white label it must be dark. #4DA3FF reads
8.00:1 as text on black but 2.63:1 behind white text, so StatusInfo /
AccentColor cover the foreground role and AccentFill covers
.borderedProminent. AppOnAccent is the label colour for a solid fill and
flips by scheme.

Colours live in Shared/Colors.xcassets rather than the app catalog: the
Shared folder is already a synchronized group in all three targets, so
the widget and share extension pick the palette up with no project-file
surgery. AccentColor stays in the app catalog as the global tint — and is
now actually defined, having been an empty colorset that silently left
system controls rendering in stock blue while custom chrome used cyan.

Two deliberate visual changes: orange folds into StatusWarning and pink
into StatusCritical. They encoded the same severity as the colours they
now share, and both sites also carry a text label.

Audit findings drop 15 to 14, and one of the originals turned out to be
a phantom: the Inspect contrast failure was the Run button in its
disabled state, which WCAG 1.4.3 exempts. testInspectScreen now types a
domain first so the audit measures an enabled control. Findings also
carry the offending element now, so the remaining clipped-text items name
themselves ("No Portfolio Yet", "Search domains") instead of being
anonymous.

Layout: unified · split

Docs/ACCESSIBILITY.md +58
@@ -6,6 +6,58 @@ large Dynamic Type, element descriptions, trait correctness, and Dynamic Type
6support — the same ground the accessibility pass tracked in 6support — the same ground the accessibility pass tracked in
7[issue #21](https://github.com/zerolabsco/domain-dig/issues/21) covers. 7[issue #21](https://github.com/zerolabsco/domain-dig/issues/21) covers.
8 8
9## The colour palette
10
11Semantic colours live in `Shared/Colors.xcassets`, which is inside the `Shared`
12file-system-synchronized group and therefore reaches the app, the widget, and
13the share extension automatically. `AccentColor` stays in
14`DomainDig/Assets.xcassets` because it is the system-wide tint resolved via
15`ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME`.
16
17Use the generated asset symbols — `Color(.statusCritical)`, `Color(.appSurface)`
18— never a literal. `ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS`
19is on, so these are compile-time checked; a typo will not build.
20
21Every value clears WCAG AA (4.5:1) as text on its page, on its card, **and on
22its own 16% badge tint** — the way `AppStatusBadgeView` actually draws it. The
23worst of those three is shown:
24
25| Role | Light | Dark | Worst light | Worst dark |
26| --- | --- | --- | --- | --- |
27| `StatusInfo` / `AccentColor` | `#0000FF` | `#4DA3FF` | 5.50 | 5.85 |
28| `StatusPositive` | `#146C2E` | `#30D158` | 4.65 | 7.32 |
29| `StatusWarning` | `#7A5600` | `#FFD60A` | 4.74 | 9.61 |
30| `StatusCritical` | `#B3261E` | `#FF6961` | 4.51 | 5.54 |
31| `StatusNeutral` | `#5A5A5F` | `#A1A1A6` | 4.92 | 5.88 |
32
33High Contrast variants push further in the same direction. Surfaces
34(`AppBackground`, `AppSurface`, `AppSurfaceElevated`, `AppSeparator`) carry no
35meaning, so they get Any/Dark and, where useful, High Contrast — but no status
36semantics.
37
38Why custom values instead of the system palette: **every** system colour fails
39in light mode. Measured on white — systemYellow 1.51:1, systemOrange 2.20:1,
40systemGreen 2.22:1, systemCyan 2.54:1, systemRed 3.55:1. All of them pass in
41dark mode, which is why the dark-locked app looked fine and why unlocking light
42mode is impossible without this work.
43
44### The accent has two roles, and they conflict
45
46An accent used as **text on a dark background** must be light. The same accent
47used as a **fill behind a white label** must be dark. One value cannot do both:
48`#4DA3FF` reads at 8.00:1 as text on black, but only 2.63:1 behind white text.
49
50So there are two colours:
51
52- `StatusInfo` / `AccentColor` — the accent as *foreground*: text, icons,
53 bordered-button labels, tab bar.
54- `AccentFill` — the accent as a *filled background* behind a label, used by
55 `.borderedProminent`. Stays dark in both schemes so a white label clears AA
56 (8.59:1 light, 7.56:1 dark).
57
58`AppOnAccent` is the label colour for a solid accent fill and flips by scheme —
59white on the light accent, black on the dark one.
60
9## Findings are reported, not enforced 61## Findings are reported, not enforced
10 62
11The audit surfaces violations that exist today, so failing on all of them would 63The audit surfaces violations that exist today, so failing on all of them would
@@ -85,6 +137,12 @@ pre-commit that blocks every commit. A hook routinely bypassed with
85 137
86## Notes 138## Notes
87 139
140- **Disabled controls are a false positive.** WCAG 1.4.3 exempts inactive
141 components from contrast requirements, but the audit flags them anyway. The
142 Inspect screen's Run button is disabled until a domain is typed, and auditing
143 the empty state reported a contrast failure that was never a real defect —
144 which is why `testInspectScreen` types a domain before auditing. Watch for
145 this before "fixing" a contrast finding on a disabled control.
88- Audits retry up to three times. Slower machines can miss the audit's internal 146- Audits retry up to three times. Slower machines can miss the audit's internal
89 deadline (`Audit failed to complete in time`, code `-56`), which is a tooling 147 deadline (`Audit failed to complete in time`, code `-56`), which is a tooling
90 timeout, not an app defect. A screen that still cannot be audited is reported 148 timeout, not an app defect. A screen that still cannot be audited is reported
DomainDig/Assets.xcassets/AccentColor.colorset/Contents.json +72 −5
@@ -1,11 +1,78 @@
1{ 1{
2 "colors" : [ 2 "colors": [
3 { 3 {
4 "idiom" : "universal" 4 "idiom": "universal",
5 "color": {
6 "color-space": "srgb",
7 "components": {
8 "red": "0x00",
9 "green": "0x00",
10 "blue": "0xFF",
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": "0x4D",
27 "green": "0xA3",
28 "blue": "0xFF",
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": "0x00",
45 "green": "0x00",
46 "blue": "0xCC",
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": "0x7F",
67 "green": "0xBF",
68 "blue": "0xFF",
69 "alpha": "1.000"
70 }
71 }
5 } 72 }
6 ], 73 ],
7 "info" : { 74 "info": {
8 "author" : "xcode", 75 "author": "xcode",
9 "version" : 1 76 "version": 1
10 } 77 }
11} 78}
DomainDig/BatchResultsView.swift +10 −10
@@ -13,7 +13,7 @@ struct BatchResultsView: View {
13 if viewModel.batchLookupRunning { 13 if viewModel.batchLookupRunning {
14 VStack(alignment: .trailing, spacing: 4) { 14 VStack(alignment: .trailing, spacing: 4) {
15 ProgressView(value: Double(viewModel.batchCompletedCount), total: Double(max(viewModel.batchTotalCount, 1))) 15 ProgressView(value: Double(viewModel.batchCompletedCount), total: Double(max(viewModel.batchTotalCount, 1)))
16 .tint(.cyan) 16 .tint(Color(.statusInfo))
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))
@@ -110,7 +110,7 @@ struct BatchResultRowView: View {
110 if let errorMessage = result.errorMessage { 110 if let errorMessage = result.errorMessage {
111 Text(errorMessage) 111 Text(errorMessage)
112 .font(appDensity.font(.caption2)) 112 .font(appDensity.font(.caption2))
113 .foregroundStyle(result.status == .failed ? .red : .secondary) 113 .foregroundStyle(result.status == .failed ? Color(.statusCritical) : .secondary)
114 } 114 }
115 } 115 }
116 .frame(maxWidth: .infinity, alignment: .leading) 116 .frame(maxWidth: .infinity, alignment: .leading)
@@ -138,29 +138,29 @@ struct BatchResultRowView: View {
138 title: availabilityText, 138 title: availabilityText,
139 systemImage: "exclamationmark.circle", 139 systemImage: "exclamationmark.circle",
140 foregroundColor: .secondary, 140 foregroundColor: .secondary,
141 backgroundColor: Color(.systemGray5).opacity(0.55) 141 backgroundColor: Color(.appSurfaceElevated)
142 ) 142 )
143 } 143 }
144 144
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(.systemGray5).opacity(0.55)) 148 return .init(title: "Pending", systemImage: "clock", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated))
149 case .running: 149 case .running:
150 return .init(title: "Running", systemImage: "arrow.clockwise", foregroundColor: .cyan, backgroundColor: .cyan.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:
152 if result.changeClassification == .critical || result.certificateWarningLevel == .critical || result.riskLevel == .high { 152 if result.changeClassification == .critical || result.certificateWarningLevel == .critical || result.riskLevel == .high {
153 return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: .red, backgroundColor: .red.opacity(0.16)) 153 return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
154 } 154 }
155 if result.changeClassification == .warning || result.changeSeverity == .medium || result.certificateWarningLevel == .warning { 155 if result.changeClassification == .warning || result.changeSeverity == .medium || result.certificateWarningLevel == .warning {
156 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16)) 156 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
157 } 157 }
158 if result.quickStatus == "Changed" { 158 if result.quickStatus == "Changed" {
159 return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: .cyan, backgroundColor: .cyan.opacity(0.16)) 159 return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16))
160 } 160 }
161 return .init(title: "Stable", systemImage: "checkmark.circle.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16)) 161 return .init(title: "Stable", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
162 case .failed: 162 case .failed:
163 return .init(title: "Failed", systemImage: "xmark.circle.fill", foregroundColor: .red, backgroundColor: .red.opacity(0.16)) 163 return .init(title: "Failed", systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
164 } 164 }
165 } 165 }
166} 166}
DomainDig/ContentView.swift +79 −71
@@ -167,7 +167,7 @@ struct ContentView: View {
167 } 167 }
168 .background( 168 .background(
169 LinearGradient( 169 LinearGradient(
170 colors: [Color.black, Color(.systemGray6).opacity(0.12)], 170 colors: [Color(.appBackground), Color(.appSurface)],
171 startPoint: .top, 171 startPoint: .top,
172 endPoint: .bottom 172 endPoint: .bottom
173 ) 173 )
@@ -302,7 +302,7 @@ struct ContentView: View {
302 .keyboardType(.URL) 302 .keyboardType(.URL)
303 .padding(.horizontal, 12) 303 .padding(.horizontal, 12)
304 .padding(.vertical, appDensity.metrics.controlVerticalPadding) 304 .padding(.vertical, appDensity.metrics.controlVerticalPadding)
305 .background(Color(.systemGray6)) 305 .background(Color(.appSurfaceElevated))
306 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius)) 306 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius))
307 .focused($focusedInputField, equals: .singleDomain) 307 .focused($focusedInputField, equals: .singleDomain)
308 .onSubmit { 308 .onSubmit {
@@ -320,6 +320,7 @@ struct ContentView: View {
320 .frame(minHeight: appDensity.metrics.controlMinHeight) 320 .frame(minHeight: appDensity.metrics.controlMinHeight)
321 } 321 }
322 .buttonStyle(.borderedProminent) 322 .buttonStyle(.borderedProminent)
323 .tint(Color(.accentFill))
323 .disabled(viewModel.trimmedDomain.isEmpty) 324 .disabled(viewModel.trimmedDomain.isEmpty)
324 } else { 325 } else {
325 if FeatureAccessService.hasAccess(to: .batchOperations) { 326 if FeatureAccessService.hasAccess(to: .batchOperations) {
@@ -346,7 +347,7 @@ struct ContentView: View {
346 .lineLimit(4...10) 347 .lineLimit(4...10)
347 .padding(.horizontal, 12) 348 .padding(.horizontal, 12)
348 .padding(.vertical, appDensity.metrics.controlVerticalPadding) 349 .padding(.vertical, appDensity.metrics.controlVerticalPadding)
349 .background(Color(.systemGray6)) 350 .background(Color(.appSurfaceElevated))
350 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius)) 351 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius))
351 .focused($focusedInputField, equals: .bulkDomains) 352 .focused($focusedInputField, equals: .bulkDomains)
352 353
@@ -360,6 +361,7 @@ struct ContentView: View {
360 .frame(minHeight: appDensity.metrics.controlMinHeight) 361 .frame(minHeight: appDensity.metrics.controlMinHeight)
361 } 362 }
362 .buttonStyle(.borderedProminent) 363 .buttonStyle(.borderedProminent)
364 .tint(Color(.accentFill))
363 .disabled(viewModel.bulkInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.batchLookupRunning) 365 .disabled(viewModel.bulkInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.batchLookupRunning)
364 } 366 }
365 } else { 367 } else {
@@ -549,7 +551,7 @@ struct ContentView: View {
549 } label: { 551 } label: {
550 Image(systemName: viewModel.isCurrentDomainSaved ? "bookmark.fill" : "bookmark") 552 Image(systemName: viewModel.isCurrentDomainSaved ? "bookmark.fill" : "bookmark")
551 .font(appDensity.font(.body, design: .default)) 553 .font(appDensity.font(.body, design: .default))
552 .foregroundStyle(viewModel.isCurrentDomainSaved ? .yellow : .secondary) 554 .foregroundStyle(viewModel.isCurrentDomainSaved ? Color(.statusWarning) : .secondary)
553 } 555 }
554 Menu { 556 Menu {
555 Button("Export TXT") { 557 Button("Export TXT") {
@@ -671,7 +673,7 @@ struct ContentView: View {
671 .frame(maxWidth: .infinity, alignment: .leading) 673 .frame(maxWidth: .infinity, alignment: .leading)
672 .padding(.vertical, 8) 674 .padding(.vertical, 8)
673 .padding(.horizontal, 10) 675 .padding(.horizontal, 10)
674 .background(Color(.systemGray6).opacity(0.5)) 676 .background(Color(.appSurface))
675 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius)) 677 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius))
676 } 678 }
677 } 679 }
@@ -696,7 +698,7 @@ struct ContentView: View {
696 } 698 }
697 .frame(maxWidth: .infinity, alignment: .leading) 699 .frame(maxWidth: .infinity, alignment: .leading)
698 .padding(appDensity.metrics.cardPadding) 700 .padding(appDensity.metrics.cardPadding)
699 .background(Color(.systemGray6)) 701 .background(Color(.appSurfaceElevated))
700 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius)) 702 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius))
701 } 703 }
702 704
@@ -792,7 +794,7 @@ struct SummaryView: View {
792 .frame(minHeight: appDensity.metrics.rowMinHeight + 12, alignment: .topLeading) 794 .frame(minHeight: appDensity.metrics.rowMinHeight + 12, alignment: .topLeading)
793 .frame(maxWidth: .infinity, alignment: .leading) 795 .frame(maxWidth: .infinity, alignment: .leading)
794 .padding(appDensity.metrics.cardPadding) 796 .padding(appDensity.metrics.cardPadding)
795 .background(Color(.systemGray6).opacity(0.5)) 797 .background(Color(.appSurface))
796 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius)) 798 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius))
797 } 799 }
798 } 800 }
@@ -851,22 +853,22 @@ struct RiskSummaryCardView: View {
851 private var levelColor: Color { 853 private var levelColor: Color {
852 switch report.riskAssessment.level { 854 switch report.riskAssessment.level {
853 case .low: 855 case .low:
854 return .green 856 return Color(.statusPositive)
855 case .medium: 857 case .medium:
856 return .yellow 858 return Color(.statusWarning)
857 case .high: 859 case .high:
858 return .red 860 return Color(.statusCritical)
859 } 861 }
860 } 862 }
861 863
862 private func factorColor(_ impact: RiskImpact) -> Color { 864 private func factorColor(_ impact: RiskImpact) -> Color {
863 switch impact { 865 switch impact {
864 case .positive: 866 case .positive:
865 return .green 867 return Color(.statusPositive)
866 case .neutral: 868 case .neutral:
867 return .secondary 869 return .secondary
868 case .negative: 870 case .negative:
869 return .red 871 return Color(.statusCritical)
870 } 872 }
871 } 873 }
872} 874}
@@ -888,7 +890,7 @@ struct InsightsSummaryCardView: View {
888 HStack(alignment: .top, spacing: 8) { 890 HStack(alignment: .top, spacing: 8) {
889 Image(systemName: "chart.line.uptrend.xyaxis") 891 Image(systemName: "chart.line.uptrend.xyaxis")
890 .font(appDensity.font(.caption2)) 892 .font(appDensity.font(.caption2))
891 .foregroundStyle(.cyan) 893 .foregroundStyle(Color(.statusInfo))
892 .padding(.top, 2) 894 .padding(.top, 2)
893 Text(insight) 895 Text(insight)
894 .font(appDensity.font(.caption)) 896 .font(appDensity.font(.caption))
@@ -995,13 +997,13 @@ struct LookupStatusBannerView: View {
995 private var color: Color { 997 private var color: Color {
996 switch resultSource { 998 switch resultSource {
997 case .live: 999 case .live:
998 return .green 1000 return Color(.statusPositive)
999 case .cached: 1001 case .cached:
1000 return .secondary 1002 return .secondary
1001 case .mixed: 1003 case .mixed:
1002 return .yellow 1004 return Color(.statusWarning)
1003 case .snapshot: 1005 case .snapshot:
1004 return .orange 1006 return Color(.statusWarning)
1005 } 1007 }
1006 } 1008 }
1007 1009
@@ -1029,7 +1031,7 @@ struct DomainChangeSummaryView: View {
1029 HStack { 1031 HStack {
1030 Label(summary.hasChanges ? "Changed" : "Stable", systemImage: summary.hasChanges ? "arrow.triangle.2.circlepath" : "checkmark.circle") 1032 Label(summary.hasChanges ? "Changed" : "Stable", systemImage: summary.hasChanges ? "arrow.triangle.2.circlepath" : "checkmark.circle")
1031 .font(appDensity.font(.caption)) 1033 .font(appDensity.font(.caption))
1032 .foregroundStyle(summary.hasChanges ? severityColor(summary.severity) : .green) 1034 .foregroundStyle(summary.hasChanges ? severityColor(summary.severity) : Color(.statusPositive))
1033 Spacer() 1035 Spacer()
1034 Text(summary.severity.title.uppercased()) 1036 Text(summary.severity.title.uppercased())
1035 .font(appDensity.font(.caption2)) 1037 .font(appDensity.font(.caption2))
@@ -1079,13 +1081,13 @@ struct DomainChangeSummaryView: View {
1079 if let riskScoreDelta = summary.riskScoreDelta { 1081 if let riskScoreDelta = summary.riskScoreDelta {
1080 Text("Risk delta: \(riskScoreDelta >= 0 ? "+" : "")\(riskScoreDelta)") 1082 Text("Risk delta: \(riskScoreDelta >= 0 ? "+" : "")\(riskScoreDelta)")
1081 .font(appDensity.font(.caption2)) 1083 .font(appDensity.font(.caption2))
1082 .foregroundStyle(riskScoreDelta > 0 ? .orange : .secondary) 1084 .foregroundStyle(riskScoreDelta > 0 ? Color(.statusWarning) : .secondary)
1083 } 1085 }
1084 1086
1085 if let contextNote = summary.contextNote { 1087 if let contextNote = summary.contextNote {
1086 Text(contextNote) 1088 Text(contextNote)
1087 .font(appDensity.font(.caption2)) 1089 .font(appDensity.font(.caption2))
1088 .foregroundStyle(.orange) 1090 .foregroundStyle(Color(.statusWarning))
1089 } 1091 }
1090 } 1092 }
1091 .padding(.top, 4) 1093 .padding(.top, 4)
@@ -1101,9 +1103,9 @@ struct DomainChangeSummaryView: View {
1101 case .low: 1103 case .low:
1102 return .secondary 1104 return .secondary
1103 case .medium: 1105 case .medium:
1104 return .yellow 1106 return Color(.statusWarning)
1105 case .high: 1107 case .high:
1106 return .red 1108 return Color(.statusCritical)
1107 } 1109 }
1108 } 1110 }
1109 1111
@@ -1205,7 +1207,7 @@ struct DomainDiffView: View {
1205 } 1207 }
1206 } 1208 }
1207 .padding(10) 1209 .padding(10)
1208 .background(item.hasChanges ? changeColor(for: item).opacity(0.08) : Color(.systemGray6).opacity(0.25)) 1210 .background(item.hasChanges ? changeColor(for: item).opacity(0.08) : Color(.appSurface))
1209 .cornerRadius(8) 1211 .cornerRadius(8)
1210 } 1212 }
1211 } label: { 1213 } label: {
@@ -1225,7 +1227,7 @@ struct DomainDiffView: View {
1225 .overlay { 1227 .overlay {
1226 if highlightedSectionID == section.id { 1228 if highlightedSectionID == section.id {
1227 RoundedRectangle(cornerRadius: 12) 1229 RoundedRectangle(cornerRadius: 12)
1228 .stroke(Color.cyan.opacity(0.55), lineWidth: 1) 1230 .stroke(Color(.statusInfo).opacity(0.55), lineWidth: 1)
1229 } 1231 }
1230 } 1232 }
1231 } 1233 }
@@ -1258,9 +1260,9 @@ struct DomainDiffView: View {
1258 case .low: 1260 case .low:
1259 return .blue 1261 return .blue
1260 case .medium: 1262 case .medium:
1261 return .yellow 1263 return Color(.statusWarning)
1262 case .high: 1264 case .high:
1263 return .red 1265 return Color(.statusCritical)
1264 } 1266 }
1265 } 1267 }
1266 1268
@@ -1269,9 +1271,9 @@ struct DomainDiffView: View {
1269 case .low: 1271 case .low:
1270 return .blue 1272 return .blue
1271 case .medium: 1273 case .medium:
1272 return .yellow 1274 return Color(.statusWarning)
1273 case .high: 1275 case .high:
1274 return .red 1276 return Color(.statusCritical)
1275 } 1277 }
1276 } 1278 }
1277 1279
@@ -1337,7 +1339,7 @@ struct DomainSectionView: View {
1337 CollapsibleSectionView(title: "Domain", isCollapsed: $isCollapsed) { 1339 CollapsibleSectionView(title: "Domain", isCollapsed: $isCollapsed) {
1338 if let trackedDomain { 1340 if let trackedDomain {
1339 HStack(spacing: 8) { 1341 HStack(spacing: 8) {
1340 AppStatusBadgeView(model: .init(title: "Tracked", systemImage: "eye.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16))) 1342 AppStatusBadgeView(model: .init(title: "Tracked", systemImage: "eye.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)))
1341 Button { 1343 Button {
1342 onTogglePinned() 1344 onTogglePinned()
1343 } label: { 1345 } label: {
@@ -1638,7 +1640,7 @@ struct IntelligenceSectionView: View {
1638 if item.isEphemeral { 1640 if item.isEphemeral {
1639 Text("Ephemeral") 1641 Text("Ephemeral")
1640 .font(appDensity.font(.caption2)) 1642 .font(appDensity.font(.caption2))
1641 .foregroundStyle(.yellow) 1643 .foregroundStyle(Color(.statusWarning))
1642 } 1644 }
1643 } 1645 }
1644 Text("First \(item.firstSeen.formatted(date: .abbreviated, time: .omitted)) • Last \(item.lastSeen.formatted(date: .abbreviated, time: .omitted)) • Seen \(item.recurrenceCount)x") 1646 Text("First \(item.firstSeen.formatted(date: .abbreviated, time: .omitted)) • Last \(item.lastSeen.formatted(date: .abbreviated, time: .omitted)) • Seen \(item.recurrenceCount)x")
@@ -1667,7 +1669,7 @@ struct IntelligenceSectionView: View {
1667 VStack(alignment: .leading, spacing: 8) { 1669 VStack(alignment: .leading, spacing: 8) {
1668 Text(title) 1670 Text(title)
1669 .font(appDensity.font(.subheadline, weight: .semibold)) 1671 .font(appDensity.font(.subheadline, weight: .semibold))
1670 .foregroundStyle(.cyan) 1672 .foregroundStyle(Color(.statusInfo))
1671 content() 1673 content()
1672 } 1674 }
1673 } 1675 }
@@ -1758,7 +1760,7 @@ struct SubdomainsSectionView: View {
1758 HStack { 1760 HStack {
1759 Text("\(group.label).*") 1761 Text("\(group.label).*")
1760 .font(appDensity.font(.caption)) 1762 .font(appDensity.font(.caption))
1761 .foregroundStyle(.cyan) 1763 .foregroundStyle(Color(.statusInfo))
1762 Spacer() 1764 Spacer()
1763 Text("\(group.subdomains.count)") 1765 Text("\(group.subdomains.count)")
1764 .font(appDensity.font(.caption2)) 1766 .font(appDensity.font(.caption2))
@@ -1776,10 +1778,10 @@ struct SubdomainsSectionView: View {
1776 if row.isInteresting { 1778 if row.isInteresting {
1777 Text("Interesting") 1779 Text("Interesting")
1778 .font(.system(.caption2, design: .monospaced)) 1780 .font(.system(.caption2, design: .monospaced))
1779 .foregroundStyle(.yellow) 1781 .foregroundStyle(Color(.statusWarning))
1780 .padding(.horizontal, 8) 1782 .padding(.horizontal, 8)
1781 .padding(.vertical, 4) 1783 .padding(.vertical, 4)
1782 .background(Color.yellow.opacity(0.14)) 1784 .background(Color(.statusWarning).opacity(0.14))
1783 .clipShape(Capsule()) 1785 .clipShape(Capsule())
1784 } 1786 }
1785 } 1787 }
@@ -1883,7 +1885,7 @@ struct DNSSectionView: View {
1883 Text(section.title) 1885 Text(section.title)
1884 .font(.system(.subheadline, design: .monospaced)) 1886 .font(.system(.subheadline, design: .monospaced))
1885 .fontWeight(.semibold) 1887 .fontWeight(.semibold)
1886 .foregroundStyle(.cyan) 1888 .foregroundStyle(Color(.statusInfo))
1887 1889
1888 if let message = section.message { 1890 if let message = section.message {
1889 MessageRowView(text: message.text, isError: message.isError) 1891 MessageRowView(text: message.text, isError: message.isError)
@@ -1910,7 +1912,7 @@ struct DNSSectionView: View {
1910 Text("PTR") 1912 Text("PTR")
1911 .font(.system(.subheadline, design: .monospaced)) 1913 .font(.system(.subheadline, design: .monospaced))
1912 .fontWeight(.semibold) 1914 .fontWeight(.semibold)
1913 .foregroundStyle(.cyan) 1915 .foregroundStyle(Color(.statusInfo))
1914 SectionTrustMetadataView(provenance: ptrProvenance, confidence: nil) 1916 SectionTrustMetadataView(provenance: ptrProvenance, confidence: nil)
1915 MessageRowView(text: ptrMessage.text, isError: ptrMessage.isError) 1917 MessageRowView(text: ptrMessage.text, isError: ptrMessage.isError)
1916 } 1918 }
@@ -1920,7 +1922,7 @@ struct DNSSectionView: View {
1920 HStack { 1922 HStack {
1921 Text("History") 1923 Text("History")
1922 .font(appDensity.font(.subheadline, weight: .semibold)) 1924 .font(appDensity.font(.subheadline, weight: .semibold))
1923 .foregroundStyle(.cyan) 1925 .foregroundStyle(Color(.statusInfo))
1924 Spacer() 1926 Spacer()
1925 if let onLoadHistory, history.isEmpty, !historyLoading, !showsHistoryPlaceholder { 1927 if let onLoadHistory, history.isEmpty, !historyLoading, !showsHistoryPlaceholder {
1926 Button("Load") { 1928 Button("Load") {
@@ -1990,7 +1992,7 @@ struct WebSectionView: View {
1990 HStack { 1992 HStack {
1991 Text("TLS") 1993 Text("TLS")
1992 .font(appDensity.font(.subheadline, weight: .semibold)) 1994 .font(appDensity.font(.subheadline, weight: .semibold))
1993 .foregroundStyle(.cyan) 1995 .foregroundStyle(Color(.statusInfo))
1994 Spacer() 1996 Spacer()
1995 if !sslLoading { 1997 if !sslLoading {
1996 AppStatusBadgeView(model: AppStatusFactory.tls(sslInfo: sslInfo, error: sslError)) 1998 AppStatusBadgeView(model: AppStatusFactory.tls(sslInfo: sslInfo, error: sslError))
@@ -2034,7 +2036,7 @@ struct WebSectionView: View {
2034 CardView { 2036 CardView {
2035 Text("Headers") 2037 Text("Headers")
2036 .font(appDensity.font(.subheadline, weight: .semibold)) 2038 .font(appDensity.font(.subheadline, weight: .semibold))
2037 .foregroundStyle(.cyan) 2039 .foregroundStyle(Color(.statusInfo))
2038 SectionTrustMetadataView(provenance: httpProvenance, confidence: nil) 2040 SectionTrustMetadataView(provenance: httpProvenance, confidence: nil)
2039 if headersLoading { 2041 if headersLoading {
2040 ProgressView("Fetching headers…") 2042 ProgressView("Fetching headers…")
@@ -2052,7 +2054,7 @@ struct WebSectionView: View {
2052 HStack(alignment: .top, spacing: 4) { 2054 HStack(alignment: .top, spacing: 4) {
2053 Text(header.name + ":") 2055 Text(header.name + ":")
2054 .font(appDensity.font(.caption)) 2056 .font(appDensity.font(.caption))
2055 .foregroundStyle(header.isSecurityHeader ? .yellow : .cyan) 2057 .foregroundStyle(header.isSecurityHeader ? Color(.statusWarning) : Color(.statusInfo))
2056 Text(header.value) 2058 Text(header.value)
2057 .font(appDensity.font(.caption)) 2059 .font(appDensity.font(.caption))
2058 .foregroundStyle(.primary) 2060 .foregroundStyle(.primary)
@@ -2067,7 +2069,7 @@ struct WebSectionView: View {
2067 HStack { 2069 HStack {
2068 Text("Redirects") 2070 Text("Redirects")
2069 .font(appDensity.font(.subheadline, weight: .semibold)) 2071 .font(appDensity.font(.subheadline, weight: .semibold))
2070 .foregroundStyle(.cyan) 2072 .foregroundStyle(Color(.statusInfo))
2071 Spacer() 2073 Spacer()
2072 if let finalURL { 2074 if let finalURL {
2073 AppCopyButton(value: finalURL, label: "Copy redirect URL") 2075 AppCopyButton(value: finalURL, label: "Copy redirect URL")
@@ -2093,7 +2095,7 @@ struct WebSectionView: View {
2093 .frame(width: 16, alignment: .trailing) 2095 .frame(width: 16, alignment: .trailing)
2094 Text(redirect.statusCode) 2096 Text(redirect.statusCode)
2095 .font(appDensity.font(.caption)) 2097 .font(appDensity.font(.caption))
2096 .foregroundStyle(.cyan) 2098 .foregroundStyle(Color(.statusInfo))
2097 .frame(width: 36, alignment: .leading) 2099 .frame(width: 36, alignment: .leading)
2098 Text(redirect.url) 2100 Text(redirect.url)
2099 .font(appDensity.font(.caption)) 2101 .font(appDensity.font(.caption))
@@ -2163,7 +2165,7 @@ struct EmailSectionView: View {
2163 HStack(spacing: 8) { 2165 HStack(spacing: 8) {
2164 Text(row.label) 2166 Text(row.label)
2165 .font(appDensity.font(.caption)) 2167 .font(appDensity.font(.caption))
2166 .foregroundStyle(.cyan) 2168 .foregroundStyle(Color(.statusInfo))
2167 .frame(width: 76, alignment: .leading) 2169 .frame(width: 76, alignment: .leading)
2168 AppStatusBadgeView(model: emailRowBadge(row)) 2170 AppStatusBadgeView(model: emailRowBadge(row))
2169 } 2171 }
@@ -2186,13 +2188,13 @@ struct EmailSectionView: View {
2186 private func emailRowBadge(_ row: EmailRowViewData) -> AppStatusBadgeModel { 2188 private func emailRowBadge(_ row: EmailRowViewData) -> AppStatusBadgeModel {
2187 switch row.statusTone { 2189 switch row.statusTone {
2188 case .success: 2190 case .success:
2189 return .init(title: row.status, systemImage: "checkmark.shield.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16)) 2191 return .init(title: row.status, systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
2190 case .warning: 2192 case .warning:
2191 return .init(title: row.status, systemImage: "shield.lefthalf.filled", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16)) 2193 return .init(title: row.status, systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
2192 case .failure: 2194 case .failure:
2193 return .init(title: row.status, systemImage: "minus.circle", foregroundColor: .secondary, backgroundColor: Color(.systemGray5).opacity(0.55)) 2195 return .init(title: row.status, systemImage: "minus.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated))
2194 case .primary, .secondary: 2196 case .primary, .secondary:
2195 return .init(title: row.status, systemImage: "circle", foregroundColor: .secondary, backgroundColor: Color(.systemGray5).opacity(0.55)) 2197 return .init(title: row.status, systemImage: "circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated))
2196 } 2198 }
2197 } 2199 }
2198} 2200}
@@ -2227,7 +2229,7 @@ struct NetworkSectionView: View {
2227 CardView { 2229 CardView {
2228 Text("Reachability") 2230 Text("Reachability")
2229 .font(appDensity.font(.subheadline, weight: .semibold)) 2231 .font(appDensity.font(.subheadline, weight: .semibold))
2230 .foregroundStyle(.cyan) 2232 .foregroundStyle(Color(.statusInfo))
2231 SectionTrustMetadataView(provenance: reachabilityProvenance, confidence: nil) 2233 SectionTrustMetadataView(provenance: reachabilityProvenance, confidence: nil)
2232 if reachabilityLoading { 2234 if reachabilityLoading {
2233 ProgressView("Checking ports…") 2235 ProgressView("Checking ports…")
@@ -2252,7 +2254,7 @@ struct NetworkSectionView: View {
2252 CardView(allowsHorizontalScroll: false) { 2254 CardView(allowsHorizontalScroll: false) {
2253 Text("Location") 2255 Text("Location")
2254 .font(appDensity.font(.subheadline, weight: .semibold)) 2256 .font(appDensity.font(.subheadline, weight: .semibold))
2255 .foregroundStyle(.cyan) 2257 .foregroundStyle(Color(.statusInfo))
2256 SectionTrustMetadataView(provenance: geolocationProvenance, confidence: geolocationConfidence) 2258 SectionTrustMetadataView(provenance: geolocationProvenance, confidence: geolocationConfidence)
2257 if geolocationLoading { 2259 if geolocationLoading {
2258 ProgressView("Looking up location…") 2260 ProgressView("Looking up location…")
@@ -2284,13 +2286,13 @@ struct NetworkSectionView: View {
2284 CardView(allowsHorizontalScroll: false) { 2286 CardView(allowsHorizontalScroll: false) {
2285 Text("Port Scan") 2287 Text("Port Scan")
2286 .font(appDensity.font(.subheadline, weight: .semibold)) 2288 .font(appDensity.font(.subheadline, weight: .semibold))
2287 .foregroundStyle(.cyan) 2289 .foregroundStyle(Color(.statusInfo))
2288 SectionTrustMetadataView(provenance: portScanProvenance, confidence: nil) 2290 SectionTrustMetadataView(provenance: portScanProvenance, confidence: nil)
2289 2291
2290 if isCloudflareProxied { 2292 if isCloudflareProxied {
2291 Text("Domain is behind Cloudflare's proxy. Results reflect the edge, not the origin.") 2293 Text("Domain is behind Cloudflare's proxy. Results reflect the edge, not the origin.")
2292 .font(appDensity.font(.caption2)) 2294 .font(appDensity.font(.caption2))
2293 .foregroundStyle(.orange) 2295 .foregroundStyle(Color(.statusWarning))
2294 .fixedSize(horizontal: false, vertical: true) 2296 .fixedSize(horizontal: false, vertical: true)
2295 } 2297 }
2296 2298
@@ -2314,7 +2316,7 @@ struct NetworkSectionView: View {
2314 .autocorrectionDisabled() 2316 .autocorrectionDisabled()
2315 .keyboardType(.numberPad) 2317 .keyboardType(.numberPad)
2316 .padding(10) 2318 .padding(10)
2317 .background(Color(.systemGray6).opacity(0.5)) 2319 .background(Color(.appSurface))
2318 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius)) 2320 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius))
2319 2321
2320 Button("Scan") { 2322 Button("Scan") {
@@ -2322,6 +2324,7 @@ struct NetworkSectionView: View {
2322 onScanCustomPorts() 2324 onScanCustomPorts()
2323 } 2325 }
2324 .buttonStyle(.borderedProminent) 2326 .buttonStyle(.borderedProminent)
2327 .tint(Color(.accentFill))
2325 .disabled(customPortScanLoading) 2328 .disabled(customPortScanLoading)
2326 2329
2327 if customPortScanLoading { 2330 if customPortScanLoading {
@@ -2344,13 +2347,13 @@ struct NetworkSectionView: View {
2344 private func reachabilityBadge(_ row: ReachabilityRowViewData) -> AppStatusBadgeModel { 2347 private func reachabilityBadge(_ row: ReachabilityRowViewData) -> AppStatusBadgeModel {
2345 switch row.statusTone { 2348 switch row.statusTone {
2346 case .success: 2349 case .success:
2347 return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16)) 2350 return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
2348 case .warning: 2351 case .warning:
2349 return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16)) 2352 return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
2350 case .failure: 2353 case .failure:
2351 return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: .red, backgroundColor: .red.opacity(0.16)) 2354 return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
2352 case .primary, .secondary: 2355 case .primary, .secondary:
2353 return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: .secondary, backgroundColor: Color(.systemGray5).opacity(0.55)) 2356 return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated))
2354 } 2357 }
2355 } 2358 }
2356} 2359}
@@ -2395,13 +2398,13 @@ struct PortRowsView: View {
2395 private func portBadge(_ row: PortScanRowViewData) -> AppStatusBadgeModel { 2398 private func portBadge(_ row: PortScanRowViewData) -> AppStatusBadgeModel {
2396 switch row.statusTone { 2399 switch row.statusTone {
2397 case .success: 2400 case .success:
2398 return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16)) 2401 return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
2399 case .warning: 2402 case .warning:
2400 return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16)) 2403 return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
2401 case .failure: 2404 case .failure:
2402 return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: .red, backgroundColor: .red.opacity(0.16)) 2405 return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
2403 case .primary, .secondary: 2406 case .primary, .secondary:
2404 return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: .secondary, backgroundColor: Color(.systemGray5).opacity(0.55)) 2407 return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated))
2405 } 2408 }
2406 } 2409 }
2407} 2410}
@@ -2413,7 +2416,7 @@ struct SectionTitleView: View {
2413 var body: some View { 2416 var body: some View {
2414 Text(title) 2417 Text(title)
2415 .font(appDensity.font(.headline, design: .default, weight: .semibold)) 2418 .font(appDensity.font(.headline, design: .default, weight: .semibold))
2416 .foregroundStyle(.white) 2419 .foregroundStyle(.primary)
2417 } 2420 }
2418} 2421}
2419 2422
@@ -2442,7 +2445,7 @@ struct CardView<Content: View>: View {
2442 } 2445 }
2443 .frame(maxWidth: .infinity, alignment: .leading) 2446 .frame(maxWidth: .infinity, alignment: .leading)
2444 .padding(appDensity.metrics.cardPadding) 2447 .padding(appDensity.metrics.cardPadding)
2445 .background(Color(.systemGray6).opacity(0.5)) 2448 .background(Color(.appSurface))
2446 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius)) 2449 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius))
2447 } 2450 }
2448 2451
@@ -2484,7 +2487,7 @@ struct MessageRowView: View {
2484 var body: some View { 2487 var body: some View {
2485 Label(text, systemImage: isError ? "exclamationmark.triangle.fill" : "info.circle") 2488 Label(text, systemImage: isError ? "exclamationmark.triangle.fill" : "info.circle")
2486 .font(appDensity.font(.caption)) 2489 .font(appDensity.font(.caption))
2487 .foregroundStyle(isError ? .red : .secondary) 2490 .foregroundStyle(isError ? Color(.statusCritical) : .secondary)
2488 .lineLimit(nil) 2491 .lineLimit(nil)
2489 .fixedSize(horizontal: false, vertical: true) 2492 .fixedSize(horizontal: false, vertical: true)
2490 } 2493 }
@@ -2576,6 +2579,11 @@ struct LabeledValueRow: View {
2576 } 2579 }
2577} 2580}
2578 2581
2582/// Maps a row's semantic tone onto the app palette.
2583///
2584/// See `Docs/ACCESSIBILITY.md` for the measured contrast ratios behind these
2585/// colours. Never reach for a literal (`Color(.statusCritical)`, `Color(.statusWarning)`, …) — the system
2586/// palette fails WCAG AA badly in light mode (systemYellow is 1.28:1 on white).
2579enum ResultColors { 2587enum ResultColors {
2580 static func color(for tone: ResultTone) -> Color { 2588 static func color(for tone: ResultTone) -> Color {
2581 switch tone { 2589 switch tone {
@@ -2584,11 +2592,11 @@ enum ResultColors {
2584 case .secondary: 2592 case .secondary:
2585 return .secondary 2593 return .secondary
2586 case .success: 2594 case .success:
2587 return .green 2595 return Color(.statusPositive)
2588 case .warning: 2596 case .warning:
2589 return .yellow 2597 return Color(.statusWarning)
2590 case .failure: 2598 case .failure:
2591 return .red 2599 return Color(.statusCritical)
2592 } 2600 }
2593 } 2601 }
2594} 2602}
@@ -2654,7 +2662,7 @@ struct SettingsView: View {
2654 if let errorMessage = purchaseService.errorMessage { 2662 if let errorMessage = purchaseService.errorMessage {
2655 Text(errorMessage) 2663 Text(errorMessage)
2656 .font(appDensity.font(.caption, design: .default)) 2664 .font(appDensity.font(.caption, design: .default))
2657 .foregroundStyle(.red) 2665 .foregroundStyle(Color(.statusCritical))
2658 } 2666 }
2659 } 2667 }
2660 2668
@@ -2789,7 +2797,7 @@ private struct HistoryNetworkSettingsView: View {
2789 if let customResolverError { 2797 if let customResolverError {
2790 Text(customResolverError) 2798 Text(customResolverError)
2791 .font(appDensity.font(.caption, design: .default)) 2799 .font(appDensity.font(.caption, design: .default))
2792 .foregroundStyle(.red) 2800 .foregroundStyle(Color(.statusCritical))
2793 } 2801 }
2794 } 2802 }
2795 } 2803 }
@@ -2871,7 +2879,7 @@ private struct CloudSyncSettingsView: View {
2871 if let lastErrorMessage = cloudSyncService.lastErrorMessage { 2879 if let lastErrorMessage = cloudSyncService.lastErrorMessage {
2872 Text(lastErrorMessage) 2880 Text(lastErrorMessage)
2873 .font(appDensity.font(.caption, design: .default)) 2881 .font(appDensity.font(.caption, design: .default))
2874 .foregroundStyle(.red) 2882 .foregroundStyle(Color(.statusCritical))
2875 } 2883 }
2876 } 2884 }
2877 } 2885 }
@@ -2964,7 +2972,7 @@ private struct LocalAPISettingsView: View {
2964 Spacer() 2972 Spacer()
2965 Text("\(log.statusCode)") 2973 Text("\(log.statusCode)")
2966 .font(appDensity.font(.caption, design: .default)) 2974 .font(appDensity.font(.caption, design: .default))
2967 .foregroundStyle(log.statusCode >= 400 ? .red : .secondary) 2975 .foregroundStyle(log.statusCode >= 400 ? Color(.statusCritical) : .secondary)
2968 } 2976 }
2969 2977
2970 Text(log.timestamp.formatted(date: .abbreviated, time: .standard)) 2978 Text(log.timestamp.formatted(date: .abbreviated, time: .standard))
@@ -3744,8 +3752,8 @@ extension ChangeImpactClassification {
3744 var color: Color { 3752 var color: Color {
3745 switch self { 3753 switch self {
3746 case .informational: return .secondary 3754 case .informational: return .secondary
3747 case .warning: return .yellow 3755 case .warning: return Color(.statusWarning)
3748 case .critical: return .red 3756 case .critical: return Color(.statusCritical)
3749 } 3757 }
3750 } 3758 }
3751} 3759}
DomainDig/DashboardView.swift +31 −31
@@ -22,20 +22,20 @@ struct DashboardView: View {
22 showsCardBackground: false 22 showsCardBackground: false
23 ) 23 )
24 } 24 }
25 .listRowBackground(Color(.systemGray6).opacity(0.5)) 25 .listRowBackground(Color(.appSurface))
26 } else { 26 } else {
27 Section { 27 Section {
28 LazyVGrid(columns: summaryColumns, spacing: 10) { 28 LazyVGrid(columns: summaryColumns, spacing: 10) {
29 summaryCard(title: "Total Domains", value: viewModel.portfolioDashboardData.snapshot.totalDomains, filter: .all, tint: .cyan) 29 summaryCard(title: "Total Domains", value: viewModel.portfolioDashboardData.snapshot.totalDomains, filter: .all, tint: Color(.statusInfo))
30 summaryCard(title: "Healthy", value: viewModel.portfolioDashboardData.snapshot.healthyCount, filter: .healthy, tint: .green) 30 summaryCard(title: "Healthy", value: viewModel.portfolioDashboardData.snapshot.healthyCount, filter: .healthy, tint: Color(.statusPositive))
31 summaryCard(title: "Warning", value: viewModel.portfolioDashboardData.snapshot.warningCount, filter: .warning, tint: .yellow) 31 summaryCard(title: "Warning", value: viewModel.portfolioDashboardData.snapshot.warningCount, filter: .warning, tint: Color(.statusWarning))
32 summaryCard(title: "Critical", value: viewModel.portfolioDashboardData.snapshot.criticalCount, filter: .critical, tint: .red) 32 summaryCard(title: "Critical", value: viewModel.portfolioDashboardData.snapshot.criticalCount, filter: .critical, tint: Color(.statusCritical))
33 summaryCard(title: "Changes (24h)", value: viewModel.portfolioDashboardData.snapshot.changedLast24h, filter: .changed, tint: .orange) 33 summaryCard(title: "Changes (24h)", value: viewModel.portfolioDashboardData.snapshot.changedLast24h, filter: .changed, tint: Color(.statusWarning))
34 summaryCard(title: "Unreachable", value: viewModel.portfolioDashboardData.snapshot.unreachableCount, filter: .unreachable, tint: .pink) 34 summaryCard(title: "Unreachable", value: viewModel.portfolioDashboardData.snapshot.unreachableCount, filter: .unreachable, tint: Color(.statusCritical))
35 } 35 }
36 .padding(.vertical, 4) 36 .padding(.vertical, 4)
37 } 37 }
38 .listRowBackground(Color(.systemGray6).opacity(0.5)) 38 .listRowBackground(Color(.appSurface))
39 39
40 Section("Quick Filters") { 40 Section("Quick Filters") {
41 ScrollView(.horizontal, showsIndicators: false) { 41 ScrollView(.horizontal, showsIndicators: false) {
@@ -46,10 +46,10 @@ struct DashboardView: View {
46 } label: { 46 } label: {
47 Text(filter.title) 47 Text(filter.title)
48 .font(appDensity.font(.caption, weight: .semibold)) 48 .font(appDensity.font(.caption, weight: .semibold))
49 .foregroundStyle(viewModel.dashboardFilter == filter ? Color.black : Color.white) 49 .foregroundStyle(viewModel.dashboardFilter == filter ? Color(.appOnAccent) : Color.primary)
50 .padding(.horizontal, 12) 50 .padding(.horizontal, 12)
51 .padding(.vertical, 8) 51 .padding(.vertical, 8)
52 .background(viewModel.dashboardFilter == filter ? Color.cyan : Color(.systemGray5).opacity(0.6)) 52 .background(viewModel.dashboardFilter == filter ? Color(.statusInfo) : Color(.appSurfaceElevated))
53 .clipShape(Capsule()) 53 .clipShape(Capsule())
54 } 54 }
55 .buttonStyle(.plain) 55 .buttonStyle(.plain)
@@ -58,7 +58,7 @@ struct DashboardView: View {
58 .padding(.vertical, 4) 58 .padding(.vertical, 4)
59 } 59 }
60 } 60 }
61 .listRowBackground(Color(.systemGray6).opacity(0.5)) 61 .listRowBackground(Color(.appSurface))
62 62
63 Section("Recent Activity") { 63 Section("Recent Activity") {
64 if viewModel.filteredPortfolioRecentActivity.isEmpty { 64 if viewModel.filteredPortfolioRecentActivity.isEmpty {
@@ -76,7 +76,7 @@ struct DashboardView: View {
76 } 76 }
77 } 77 }
78 } 78 }
79 .listRowBackground(Color(.systemGray6).opacity(0.5)) 79 .listRowBackground(Color(.appSurface))
80 80
81 Section("Attention Required") { 81 Section("Attention Required") {
82 if viewModel.filteredPortfolioAttentionRequired.isEmpty { 82 if viewModel.filteredPortfolioAttentionRequired.isEmpty {
@@ -94,7 +94,7 @@ struct DashboardView: View {
94 } 94 }
95 } 95 }
96 } 96 }
97 .listRowBackground(Color(.systemGray6).opacity(0.5)) 97 .listRowBackground(Color(.appSurface))
98 98
99 Section("Expiring Soon") { 99 Section("Expiring Soon") {
100 if viewModel.filteredPortfolioExpiringSoon.isEmpty { 100 if viewModel.filteredPortfolioExpiringSoon.isEmpty {
@@ -110,7 +110,7 @@ struct DashboardView: View {
110 } 110 }
111 } 111 }
112 } 112 }
113 .listRowBackground(Color(.systemGray6).opacity(0.5)) 113 .listRowBackground(Color(.appSurface))
114 114
115 Section("Portfolio List") { 115 Section("Portfolio List") {
116 if viewModel.filteredPortfolioGroups.isEmpty { 116 if viewModel.filteredPortfolioGroups.isEmpty {
@@ -150,11 +150,11 @@ struct DashboardView: View {
150 } 150 }
151 } 151 }
152 } 152 }
153 .listRowBackground(Color(.systemGray6).opacity(0.5)) 153 .listRowBackground(Color(.appSurface))
154 } 154 }
155 } 155 }
156 .scrollContentBackground(.hidden) 156 .scrollContentBackground(.hidden)
157 .background(Color.black) 157 .background(Color(.appBackground))
158 .navigationTitle("Dashboard") 158 .navigationTitle("Dashboard")
159 .searchable(text: $viewModel.dashboardSearchText, prompt: "Search portfolio") 159 .searchable(text: $viewModel.dashboardSearchText, prompt: "Search portfolio")
160 .toolbar { 160 .toolbar {
@@ -196,7 +196,7 @@ struct DashboardView: View {
196 .foregroundStyle(.secondary) 196 .foregroundStyle(.secondary)
197 Text("\(value)") 197 Text("\(value)")
198 .font(.system(size: 28, weight: .bold, design: .rounded)) 198 .font(.system(size: 28, weight: .bold, design: .rounded))
199 .foregroundStyle(.white) 199 .foregroundStyle(.primary)
200 HStack { 200 HStack {
201 Circle() 201 Circle()
202 .fill(tint) 202 .fill(tint)
@@ -218,13 +218,13 @@ struct DashboardView: View {
218 if viewModel.dashboardFilter == filter { 218 if viewModel.dashboardFilter == filter {
219 return AnyShapeStyle( 219 return AnyShapeStyle(
220 LinearGradient( 220 LinearGradient(
221 colors: [Color.cyan.opacity(0.28), Color.cyan.opacity(0.12)], 221 colors: [Color(.statusInfo).opacity(0.28), Color(.statusInfo).opacity(0.12)],
222 startPoint: .topLeading, 222 startPoint: .topLeading,
223 endPoint: .bottomTrailing 223 endPoint: .bottomTrailing
224 ) 224 )
225 ) 225 )
226 } 226 }
227 return AnyShapeStyle(Color(.systemGray5).opacity(0.45)) 227 return AnyShapeStyle(Color(.appSurfaceElevated))
228 } 228 }
229 229
230 private func dashboardEmptyRow(_ message: String) -> some View { 230 private func dashboardEmptyRow(_ message: String) -> some View {
@@ -243,15 +243,15 @@ struct DashboardView: View {
243 243
244 if criticalCount > 0 { 244 if criticalCount > 0 {
245 title = "\(criticalCount) critical" 245 title = "\(criticalCount) critical"
246 color = .red 246 color = Color(.statusCritical)
247 systemImage = "exclamationmark.octagon.fill" 247 systemImage = "exclamationmark.octagon.fill"
248 } else if warningCount > 0 { 248 } else if warningCount > 0 {
249 title = "\(warningCount) warning" 249 title = "\(warningCount) warning"
250 color = .yellow 250 color = Color(.statusWarning)
251 systemImage = "exclamationmark.triangle.fill" 251 systemImage = "exclamationmark.triangle.fill"
252 } else { 252 } else {
253 title = "Healthy" 253 title = "Healthy"
254 color = .green 254 color = Color(.statusPositive)
255 systemImage = "checkmark.circle.fill" 255 systemImage = "checkmark.circle.fill"
256 } 256 }
257 257
@@ -297,11 +297,11 @@ private struct PortfolioActivityRow: View {
297 private var iconColor: Color { 297 private var iconColor: Color {
298 switch item.health { 298 switch item.health {
299 case .healthy: 299 case .healthy:
300 return .cyan 300 return Color(.statusInfo)
301 case .warning: 301 case .warning:
302 return .yellow 302 return Color(.statusWarning)
303 case .critical: 303 case .critical:
304 return .red 304 return Color(.statusCritical)
305 } 305 }
306 } 306 }
307} 307}
@@ -332,11 +332,11 @@ private struct PortfolioAttentionRow: View {
332 private var badgeModel: AppStatusBadgeModel { 332 private var badgeModel: AppStatusBadgeModel {
333 switch item.health { 333 switch item.health {
334 case .healthy: 334 case .healthy:
335 return .init(title: "Healthy", systemImage: "checkmark.circle.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16)) 335 return .init(title: "Healthy", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
336 case .warning: 336 case .warning:
337 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16)) 337 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
338 case .critical: 338 case .critical:
339 return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: .red, backgroundColor: .red.opacity(0.16)) 339 return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
340 } 340 }
341 } 341 }
342} 342}
@@ -371,11 +371,11 @@ private struct PortfolioExpiryRow: View {
371 private var badgeModel: AppStatusBadgeModel { 371 private var badgeModel: AppStatusBadgeModel {
372 switch state.certificateExpiryState { 372 switch state.certificateExpiryState {
373 case .none: 373 case .none:
374 return .init(title: "Healthy", systemImage: "lock.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16)) 374 return .init(title: "Healthy", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
375 case .warning: 375 case .warning:
376 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16)) 376 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
377 case .critical: 377 case .critical:
378 return .init(title: "Critical", systemImage: "xmark.octagon.fill", foregroundColor: .red, backgroundColor: .red.opacity(0.16)) 378 return .init(title: "Critical", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
379 } 379 }
380 } 380 }
381} 381}
DomainDig/DomainCompareView.swift +2 −2
@@ -63,11 +63,11 @@ struct DomainCompareView: View {
63 showsCardBackground: false 63 showsCardBackground: false
64 ) 64 )
65 } 65 }
66 .listRowBackground(Color(.systemGray6).opacity(0.5)) 66 .listRowBackground(Color(.appSurface))
67 } 67 }
68 } 68 }
69 .scrollContentBackground(.hidden) 69 .scrollContentBackground(.hidden)
70 .background(Color.black) 70 .background(Color(.appBackground))
71 .navigationTitle("Compare Domains") 71 .navigationTitle("Compare Domains")
72 } 72 }
73 73
DomainDig/DomainDig/AuditViews.swift +24 −24
@@ -27,7 +27,7 @@ struct AuditListView: View {
27 showsCardBackground: false 27 showsCardBackground: false
28 ) 28 )
29 } 29 }
30 .listRowBackground(Color(.systemGray6).opacity(0.5)) 30 .listRowBackground(Color(.appSurface))
31 } else { 31 } else {
32 Section("Audit Domains") { 32 Section("Audit Domains") {
33 ForEach(groupedSessions, id: \.domain) { group in 33 ForEach(groupedSessions, id: \.domain) { group in
@@ -65,11 +65,11 @@ struct AuditListView: View {
65 } 65 }
66 } 66 }
67 } 67 }
68 .listRowBackground(Color(.systemGray6).opacity(0.5)) 68 .listRowBackground(Color(.appSurface))
69 } 69 }
70 } 70 }
71 .scrollContentBackground(.hidden) 71 .scrollContentBackground(.hidden)
72 .background(Color.black) 72 .background(Color(.appBackground))
73 .navigationTitle("Audit Mode") 73 .navigationTitle("Audit Mode")
74 .toolbar { 74 .toolbar {
75 if !viewModel.searchedDomain.isEmpty { 75 if !viewModel.searchedDomain.isEmpty {
@@ -132,7 +132,7 @@ struct AuditDomainTimelineView: View {
132 } 132 }
133 } 133 }
134 } 134 }
135 .listRowBackground(Color(.systemGray6).opacity(0.5)) 135 .listRowBackground(Color(.appSurface))
136 136
137 Section("Trend") { 137 Section("Trend") {
138 ForEach(viewModel.auditTimeline(for: domain)) { point in 138 ForEach(viewModel.auditTimeline(for: domain)) { point in
@@ -152,10 +152,10 @@ struct AuditDomainTimelineView: View {
152 .padding(.vertical, 2) 152 .padding(.vertical, 2)
153 } 153 }
154 } 154 }
155 .listRowBackground(Color(.systemGray6).opacity(0.5)) 155 .listRowBackground(Color(.appSurface))
156 } 156 }
157 .scrollContentBackground(.hidden) 157 .scrollContentBackground(.hidden)
158 .background(Color.black) 158 .background(Color(.appBackground))
159 .navigationTitle(domain) 159 .navigationTitle(domain)
160 .toolbar { 160 .toolbar {
161 ToolbarItem(placement: .topBarTrailing) { 161 ToolbarItem(placement: .topBarTrailing) {
@@ -218,7 +218,7 @@ struct AuditSessionDetailView: View {
218 .foregroundStyle(.secondary) 218 .foregroundStyle(.secondary)
219 } 219 }
220 } 220 }
221 .listRowBackground(Color(.systemGray6).opacity(0.5)) 221 .listRowBackground(Color(.appSurface))
222 222
223 Section("Audit Summary") { 223 Section("Audit Summary") {
224 LabeledContent("Findings", value: "\(session.findings.count)") 224 LabeledContent("Findings", value: "\(session.findings.count)")
@@ -228,7 +228,7 @@ struct AuditSessionDetailView: View {
228 LabeledContent("Highest Severity", value: severity.title) 228 LabeledContent("Highest Severity", value: severity.title)
229 } 229 }
230 } 230 }
231 .listRowBackground(Color(.systemGray6).opacity(0.5)) 231 .listRowBackground(Color(.appSurface))
232 232
233 Section("Reviewer Notes") { 233 Section("Reviewer Notes") {
234 TextEditor(text: $notesDraft) 234 TextEditor(text: $notesDraft)
@@ -240,7 +240,7 @@ struct AuditSessionDetailView: View {
240 viewModel.updateAuditNotes(notesDraft, sessionID: session.id) 240 viewModel.updateAuditNotes(notesDraft, sessionID: session.id)
241 } 241 }
242 } 242 }
243 .listRowBackground(Color(.systemGray6).opacity(0.5)) 243 .listRowBackground(Color(.appSurface))
244 244
245 Section("Checklist") { 245 Section("Checklist") {
246 ForEach(session.checklist) { item in 246 ForEach(session.checklist) { item in
@@ -249,7 +249,7 @@ struct AuditSessionDetailView: View {
249 } label: { 249 } label: {
250 HStack(alignment: .top, spacing: 10) { 250 HStack(alignment: .top, spacing: 10) {
251 Image(systemName: item.isComplete ? "checkmark.circle.fill" : "circle") 251 Image(systemName: item.isComplete ? "checkmark.circle.fill" : "circle")
252 .foregroundStyle(item.isComplete ? Color.green : .secondary) 252 .foregroundStyle(item.isComplete ? Color(.statusPositive) : .secondary)
253 VStack(alignment: .leading, spacing: 4) { 253 VStack(alignment: .leading, spacing: 4) {
254 Text(item.title) 254 Text(item.title)
255 .font(appDensity.font(.callout)) 255 .font(appDensity.font(.callout))
@@ -264,7 +264,7 @@ struct AuditSessionDetailView: View {
264 .buttonStyle(.plain) 264 .buttonStyle(.plain)
265 } 265 }
266 } 266 }
267 .listRowBackground(Color(.systemGray6).opacity(0.5)) 267 .listRowBackground(Color(.appSurface))
268 268
269 Section("Findings") { 269 Section("Findings") {
270 if session.findings.isEmpty { 270 if session.findings.isEmpty {
@@ -301,7 +301,7 @@ struct AuditSessionDetailView: View {
301 } 301 }
302 } 302 }
303 } 303 }
304 .listRowBackground(Color(.systemGray6).opacity(0.5)) 304 .listRowBackground(Color(.appSurface))
305 305
306 Section("Evidence Snapshot") { 306 Section("Evidence Snapshot") {
307 LabeledContent("Availability", value: availabilityTitle(session.evidence.report.availability)) 307 LabeledContent("Availability", value: availabilityTitle(session.evidence.report.availability))
@@ -311,7 +311,7 @@ struct AuditSessionDetailView: View {
311 LabeledContent("Reachability", value: session.evidence.report.network.reachabilitySummary) 311 LabeledContent("Reachability", value: session.evidence.report.network.reachabilitySummary)
312 LabeledContent("Registrar", value: session.evidence.report.ownership?.registrar ?? "Unavailable") 312 LabeledContent("Registrar", value: session.evidence.report.ownership?.registrar ?? "Unavailable")
313 } 313 }
314 .listRowBackground(Color(.systemGray6).opacity(0.5)) 314 .listRowBackground(Color(.appSurface))
315 315
316 if !session.evidence.historicalContext.isEmpty { 316 if !session.evidence.historicalContext.isEmpty {
317 Section("Historical Context") { 317 Section("Historical Context") {
@@ -325,7 +325,7 @@ struct AuditSessionDetailView: View {
325 } 325 }
326 } 326 }
327 } 327 }
328 .listRowBackground(Color(.systemGray6).opacity(0.5)) 328 .listRowBackground(Color(.appSurface))
329 } 329 }
330 330
331 Section("Audit Timeline") { 331 Section("Audit Timeline") {
@@ -345,10 +345,10 @@ struct AuditSessionDetailView: View {
345 } 345 }
346 } 346 }
347 } 347 }
348 .listRowBackground(Color(.systemGray6).opacity(0.5)) 348 .listRowBackground(Color(.appSurface))
349 } 349 }
350 .scrollContentBackground(.hidden) 350 .scrollContentBackground(.hidden)
351 .background(Color.black) 351 .background(Color(.appBackground))
352 .navigationTitle("Audit Session") 352 .navigationTitle("Audit Session")
353 .toolbar { 353 .toolbar {
354 ToolbarItemGroup(placement: .topBarTrailing) { 354 ToolbarItemGroup(placement: .topBarTrailing) {
@@ -398,7 +398,7 @@ struct AuditSessionDetailView: View {
398 } 398 }
399 } else { 399 } else {
400 ContentUnavailableView("Audit Session Missing", systemImage: "exclamationmark.triangle") 400 ContentUnavailableView("Audit Session Missing", systemImage: "exclamationmark.triangle")
401 .background(Color.black) 401 .background(Color(.appBackground))
402 } 402 }
403 } 403 }
404 .preferredColorScheme(.dark) 404 .preferredColorScheme(.dark)
@@ -477,7 +477,7 @@ private struct AuditFindingEditorView: View {
477 Text(area.title) 477 Text(area.title)
478 Spacer() 478 Spacer()
479 Image(systemName: selectedAreas.contains(area) ? "checkmark.circle.fill" : "circle") 479 Image(systemName: selectedAreas.contains(area) ? "checkmark.circle.fill" : "circle")
480 .foregroundStyle(selectedAreas.contains(area) ? Color.green : .secondary) 480 .foregroundStyle(selectedAreas.contains(area) ? Color(.statusPositive) : .secondary)
481 } 481 }
482 } 482 }
483 .buttonStyle(.plain) 483 .buttonStyle(.plain)
@@ -528,11 +528,11 @@ private func auditStatusBadge(_ status: AuditStatus) -> some View {
528 let model: AppStatusBadgeModel 528 let model: AppStatusBadgeModel
529 switch status { 529 switch status {
530 case .draft: 530 case .draft:
531 model = .init(title: "Draft", systemImage: "square.and.pencil", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16)) 531 model = .init(title: "Draft", systemImage: "square.and.pencil", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
532 case .inReview: 532 case .inReview:
533 model = .init(title: "In Review", systemImage: "doc.text.magnifyingglass", foregroundColor: .cyan, backgroundColor: .cyan.opacity(0.16)) 533 model = .init(title: "In Review", systemImage: "doc.text.magnifyingglass", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16))
534 case .complete: 534 case .complete:
535 model = .init(title: "Complete", systemImage: "checkmark.seal.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16)) 535 model = .init(title: "Complete", systemImage: "checkmark.seal.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
536 } 536 }
537 return AppStatusBadgeView(model: model) 537 return AppStatusBadgeView(model: model)
538} 538}
@@ -543,11 +543,11 @@ private func findingSeverityBadge(_ severity: AuditFindingSeverity) -> some View
543 case .informational: 543 case .informational:
544 color = .secondary 544 color = .secondary
545 case .low: 545 case .low:
546 color = .green 546 color = Color(.statusPositive)
547 case .medium: 547 case .medium:
548 color = .yellow 548 color = Color(.statusWarning)
549 case .high: 549 case .high:
550 color = .red 550 color = Color(.statusCritical)
551 } 551 }
552 return AppStatusBadgeView( 552 return AppStatusBadgeView(
553 model: .init( 553 model: .init(
DomainDig/DomainDigUI.swift +21 −21
@@ -92,48 +92,48 @@ enum AppStatusFactory {
92 static func availability(_ status: DomainAvailabilityStatus?) -> AppStatusBadgeModel { 92 static func availability(_ status: DomainAvailabilityStatus?) -> AppStatusBadgeModel {
93 switch status { 93 switch status {
94 case .available: 94 case .available:
95 return .init(title: "Available", systemImage: "checkmark.circle.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16)) 95 return .init(title: "Available", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
96 case .registered: 96 case .registered:
97 return .init(title: "Registered", systemImage: "circle.fill", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16)) 97 return .init(title: "Registered", systemImage: "circle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
98 case .unknown, .none: 98 case .unknown, .none:
99 return .init(title: "Unknown", systemImage: "questionmark.circle", foregroundColor: .secondary, backgroundColor: Color(.systemGray5).opacity(0.55)) 99 return .init(title: "Unknown", systemImage: "questionmark.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated))
100 } 100 }
101 } 101 }
102 102
103 static func tls(sslInfo: SSLCertificateInfo?, error: String?) -> AppStatusBadgeModel { 103 static func tls(sslInfo: SSLCertificateInfo?, error: String?) -> AppStatusBadgeModel {
104 if error != nil || sslInfo == nil { 104 if error != nil || sslInfo == nil {
105 return .init(title: "Invalid", systemImage: "xmark.octagon.fill", foregroundColor: .red, backgroundColor: .red.opacity(0.16)) 105 return .init(title: "Invalid", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
106 } 106 }
107 if let sslInfo, sslInfo.daysUntilExpiry <= 14 { 107 if let sslInfo, sslInfo.daysUntilExpiry <= 14 {
108 return .init(title: "Expiring", systemImage: "exclamationmark.triangle.fill", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16)) 108 return .init(title: "Expiring", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
109 } 109 }
110 return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16)) 110 return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
111 } 111 }
112 112
113 static func email(_ result: EmailSecurityResult?, error: String?) -> AppStatusBadgeModel { 113 static func email(_ result: EmailSecurityResult?, error: String?) -> AppStatusBadgeModel {
114 guard error == nil, let result else { 114 guard error == nil, let result else {
115 return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: .secondary, backgroundColor: Color(.systemGray5).opacity(0.55)) 115 return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated))
116 } 116 }
117 117
118 let foundCount = [result.spf.found, result.dmarc.found, result.dkim.found].filter { $0 }.count 118 let foundCount = [result.spf.found, result.dmarc.found, result.dkim.found].filter { $0 }.count
119 switch foundCount { 119 switch foundCount {
120 case 3: 120 case 3:
121 return .init(title: "Secure", systemImage: "checkmark.shield.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16)) 121 return .init(title: "Secure", systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
122 case 1, 2: 122 case 1, 2:
123 return .init(title: "Partial", systemImage: "shield.lefthalf.filled", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16)) 123 return .init(title: "Partial", systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
124 default: 124 default:
125 return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: .secondary, backgroundColor: Color(.systemGray5).opacity(0.55)) 125 return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated))
126 } 126 }
127 } 127 }
128 128
129 static func change(_ summary: DomainChangeSummary?) -> AppStatusBadgeModel { 129 static func change(_ summary: DomainChangeSummary?) -> AppStatusBadgeModel {
130 guard let summary else { 130 guard let summary else {
131 return .init(title: "Unchanged", systemImage: "circle", foregroundColor: .secondary, backgroundColor: Color(.systemGray5).opacity(0.55)) 131 return .init(title: "Unchanged", systemImage: "circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated))
132 } 132 }
133 if summary.hasChanges { 133 if summary.hasChanges {
134 return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: .cyan, backgroundColor: .cyan.opacity(0.16)) 134 return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16))
135 } 135 }
136 return .init(title: "Unchanged", systemImage: "checkmark.circle", foregroundColor: .secondary, backgroundColor: Color(.systemGray5).opacity(0.55)) 136 return .init(title: "Unchanged", systemImage: "checkmark.circle", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated))
137 } 137 }
138} 138}
139 139
@@ -184,9 +184,9 @@ struct AppCopyButton: View {
184 } label: { 184 } label: {
185 Image(systemName: didCopy ? "checkmark" : "doc.on.doc") 185 Image(systemName: didCopy ? "checkmark" : "doc.on.doc")
186 .font(appDensity.font(.caption)) 186 .font(appDensity.font(.caption))
187 .foregroundStyle(didCopy ? Color.green : .secondary) 187 .foregroundStyle(didCopy ? Color(.statusPositive) : .secondary)
188 .frame(width: 30, height: 30) 188 .frame(width: 30, height: 30)
189 .background(Color(.systemGray5).opacity(0.35)) 189 .background(Color(.appSurfaceElevated))
190 .clipShape(RoundedRectangle(cornerRadius: 8)) 190 .clipShape(RoundedRectangle(cornerRadius: 8))
191 } 191 }
192 .buttonStyle(.plain) 192 .buttonStyle(.plain)
@@ -264,11 +264,11 @@ struct EmptyStateCardView: View {
264 264
265 Text(suggestion) 265 Text(suggestion)
266 .font(appDensity.font(.caption)) 266 .font(appDensity.font(.caption))
267 .foregroundStyle(.cyan) 267 .foregroundStyle(Color(.statusInfo))
268 } 268 }
269 .frame(maxWidth: .infinity, alignment: .leading) 269 .frame(maxWidth: .infinity, alignment: .leading)
270 .padding(appDensity.metrics.cardPadding) 270 .padding(appDensity.metrics.cardPadding)
271 .background(showsCardBackground ? Color(.systemGray6).opacity(0.45) : Color.clear) 271 .background(showsCardBackground ? Color(.appSurface) : Color.clear)
272 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius)) 272 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius))
273 } 273 }
274} 274}
@@ -307,7 +307,7 @@ struct CollapsibleSectionView<HeaderTrailing: View, Content: View>: View {
307 VStack(alignment: .leading, spacing: 3) { 307 VStack(alignment: .leading, spacing: 3) {
308 Text(title) 308 Text(title)
309 .font(appDensity.font(.headline, design: .default, weight: .semibold)) 309 .font(appDensity.font(.headline, design: .default, weight: .semibold))
310 .foregroundStyle(.white) 310 .foregroundStyle(.primary)
311 if let subtitle { 311 if let subtitle {
312 Text(subtitle) 312 Text(subtitle)
313 .font(appDensity.font(.caption)) 313 .font(appDensity.font(.caption))
@@ -346,7 +346,7 @@ struct TagChipRowView: View {
346 .font(.caption) 346 .font(.caption)
347 .padding(.horizontal, 10) 347 .padding(.horizontal, 10)
348 .padding(.vertical, 5) 348 .padding(.vertical, 5)
349 .background(Color(.systemGray5).opacity(0.6), in: Capsule()) 349 .background(Color(.appSurfaceElevated), in: Capsule())
350 } 350 }
351 } 351 }
352 } 352 }
@@ -380,8 +380,8 @@ struct TagFilterChipRowView: View {
380 .font(.caption) 380 .font(.caption)
381 .padding(.horizontal, 10) 381 .padding(.horizontal, 10)
382 .padding(.vertical, 5) 382 .padding(.vertical, 5)
383 .background(isSelected ? Color.cyan.opacity(0.3) : Color(.systemGray5).opacity(0.6), in: Capsule()) 383 .background(isSelected ? Color(.statusInfo).opacity(0.3) : Color(.appSurfaceElevated), in: Capsule())
384 .foregroundStyle(isSelected ? Color.cyan : Color.primary) 384 .foregroundStyle(isSelected ? Color(.statusInfo) : Color.primary)
385 } 385 }
386 .buttonStyle(.plain) 386 .buttonStyle(.plain)
387 } 387 }
DomainDig/HistoryView.swift +9 −9
@@ -25,7 +25,7 @@ struct HistoryView: View {
25 systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90", 25 systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90",
26 showsCardBackground: false 26 showsCardBackground: false
27 ) 27 )
28 .listRowBackground(Color(.systemGray6).opacity(0.5)) 28 .listRowBackground(Color(.appSurface))
29 } else { 29 } else {
30 Section("Domains") { 30 Section("Domains") {
31 ForEach(domainSummaries, id: \.domain) { item in 31 ForEach(domainSummaries, id: \.domain) { item in
@@ -55,21 +55,21 @@ struct HistoryView: View {
55 model: .init( 55 model: .init(
56 title: severity.title, 56 title: severity.title,
57 systemImage: "arrow.triangle.2.circlepath", 57 systemImage: "arrow.triangle.2.circlepath",
58 foregroundColor: severity == .high ? .red : .yellow, 58 foregroundColor: severity == .high ? Color(.statusCritical) : Color(.statusWarning),
59 backgroundColor: (severity == .high ? Color.red : .yellow).opacity(0.16) 59 backgroundColor: (severity == .high ? Color(.statusCritical) : Color(.statusWarning)).opacity(0.16)
60 ) 60 )
61 ) 61 )
62 } 62 }
63 } 63 }
64 } 64 }
65 } 65 }
66 .listRowBackground(Color(.systemGray6).opacity(0.5)) 66 .listRowBackground(Color(.appSurface))
67 } 67 }
68 } 68 }
69 } 69 }
70 } 70 }
71 .scrollContentBackground(.hidden) 71 .scrollContentBackground(.hidden)
72 .background(Color.black) 72 .background(Color(.appBackground))
73 .navigationTitle("History") 73 .navigationTitle("History")
74 .searchable(text: $viewModel.timelineDomainFilter, prompt: "Search domains") 74 .searchable(text: $viewModel.timelineDomainFilter, prompt: "Search domains")
75 .toolbar { 75 .toolbar {
@@ -321,7 +321,7 @@ struct HistoryDetailView: View {
321 .padding(.horizontal) 321 .padding(.horizontal)
322 .padding(.bottom, 32) 322 .padding(.bottom, 32)
323 } 323 }
324 .background(Color.black) 324 .background(Color(.appBackground))
325 .navigationTitle(entry.domain) 325 .navigationTitle(entry.domain)
326 .toolbar { 326 .toolbar {
327 ToolbarItemGroup(placement: .topBarTrailing) { 327 ToolbarItemGroup(placement: .topBarTrailing) {
@@ -390,12 +390,12 @@ struct HistoryDetailView: View {
390 if let mismatchNote = viewModel.resolverMismatchNote(for: entry) { 390 if let mismatchNote = viewModel.resolverMismatchNote(for: entry) {
391 Text(mismatchNote) 391 Text(mismatchNote)
392 .font(appDensity.font(.caption2)) 392 .font(appDensity.font(.caption2))
393 .foregroundStyle(.orange) 393 .foregroundStyle(Color(.statusWarning))
394 } 394 }
395 if entry.isPartialSnapshot { 395 if entry.isPartialSnapshot {
396 Text("Partial snapshot: \(entry.validationIssues.joined(separator: " | "))") 396 Text("Partial snapshot: \(entry.validationIssues.joined(separator: " | "))")
397 .font(appDensity.font(.caption2)) 397 .font(appDensity.font(.caption2))
398 .foregroundStyle(.yellow) 398 .foregroundStyle(Color(.statusWarning))
399 } 399 }
400 if let note = entry.note, !note.isEmpty { 400 if let note = entry.note, !note.isEmpty {
401 Text(note) 401 Text(note)
@@ -406,7 +406,7 @@ struct HistoryDetailView: View {
406 .foregroundStyle(.secondary) 406 .foregroundStyle(.secondary)
407 .padding(8) 407 .padding(8)
408 .frame(maxWidth: .infinity, alignment: .leading) 408 .frame(maxWidth: .infinity, alignment: .leading)
409 .background(Color(.systemGray6).opacity(0.3)) 409 .background(Color(.appSurface))
410 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius)) 410 .clipShape(RoundedRectangle(cornerRadius: appDensity.metrics.cardCornerRadius))
411 .padding(.vertical, 12) 411 .padding(.vertical, 12)
412 } 412 }
DomainDig/IntegrationsView.swift +3 −3
@@ -52,7 +52,7 @@ struct IntegrationsSettingsView: View {
52 if !target.isEnabled { 52 if !target.isEnabled {
53 Text("Disabled") 53 Text("Disabled")
54 .font(.caption2) 54 .font(.caption2)
55 .foregroundStyle(.orange) 55 .foregroundStyle(Color(.statusWarning))
56 } 56 }
57 } 57 }
58 } 58 }
@@ -158,7 +158,7 @@ private struct IntegrationDetailView: View {
158 .foregroundStyle(.secondary) 158 .foregroundStyle(.secondary)
159 159
160 if let failureReason = record.failureReason { 160 if let failureReason = record.failureReason {
161 let failureColor: Color = record.status == .skipped ? .secondary : .red 161 let failureColor: Color = record.status == .skipped ? .secondary : Color(.statusCritical)
162 Text(failureReason) 162 Text(failureReason)
163 .font(.caption) 163 .font(.caption)
164 .foregroundStyle(failureColor) 164 .foregroundStyle(failureColor)
@@ -324,7 +324,7 @@ private struct IntegrationEditorView: View {
324 Section { 324 Section {
325 Text(validationMessage) 325 Text(validationMessage)
326 .font(.caption) 326 .font(.caption)
327 .foregroundStyle(.red) 327 .foregroundStyle(Color(.statusCritical))
328 } 328 }
329 } 329 }
330 } 330 }
DomainDig/MonitoringView.swift +14 −14
@@ -41,7 +41,7 @@ struct MonitoringView: View {
41 } 41 }
42 .padding(.vertical, 4) 42 .padding(.vertical, 4)
43 } 43 }
44 .listRowBackground(Color(.systemGray6).opacity(0.5)) 44 .listRowBackground(Color(.appSurface))
45 45
46 if viewModel.monitoringLogs.isEmpty { 46 if viewModel.monitoringLogs.isEmpty {
47 Section { 47 Section {
@@ -53,7 +53,7 @@ struct MonitoringView: View {
53 showsCardBackground: false 53 showsCardBackground: false
54 ) 54 )
55 } 55 }
56 .listRowBackground(Color(.systemGray6).opacity(0.5)) 56 .listRowBackground(Color(.appSurface))
57 } else { 57 } else {
58 Section("Recent Runs") { 58 Section("Recent Runs") {
59 ForEach(viewModel.monitoringLogs) { log in 59 ForEach(viewModel.monitoringLogs) { log in
@@ -78,10 +78,10 @@ struct MonitoringView: View {
78 HStack(spacing: 8) { 78 HStack(spacing: 8) {
79 metricBadge(title: "\(log.domainsChecked) checked") 79 metricBadge(title: "\(log.domainsChecked) checked")
80 if log.changesFound > 0 { 80 if log.changesFound > 0 {
81 metricBadge(title: "\(log.changesFound) changed", tint: .orange) 81 metricBadge(title: "\(log.changesFound) changed", tint: Color(.statusWarning))
82 } 82 }
83 if log.alertsTriggered > 0 { 83 if log.alertsTriggered > 0 {
84 metricBadge(title: "\(log.alertsTriggered) alerts", tint: .red) 84 metricBadge(title: "\(log.alertsTriggered) alerts", tint: Color(.statusCritical))
85 } 85 }
86 } 86 }
87 } 87 }
@@ -89,11 +89,11 @@ struct MonitoringView: View {
89 } 89 }
90 } 90 }
91 } 91 }
92 .listRowBackground(Color(.systemGray6).opacity(0.5)) 92 .listRowBackground(Color(.appSurface))
93 } 93 }
94 } 94 }
95 .scrollContentBackground(.hidden) 95 .scrollContentBackground(.hidden)
96 .background(Color.black) 96 .background(Color(.appBackground))
97 .navigationTitle("Monitoring") 97 .navigationTitle("Monitoring")
98 .preferredColorScheme(.dark) 98 .preferredColorScheme(.dark)
99 .onAppear { 99 .onAppear {
@@ -101,7 +101,7 @@ struct MonitoringView: View {
101 } 101 }
102 } 102 }
103 103
104 private func metricBadge(title: String, tint: Color = .cyan) -> some View { 104 private func metricBadge(title: String, tint: Color = Color(.statusInfo)) -> some View {
105 Text(title) 105 Text(title)
106 .font(appDensity.font(.caption2)) 106 .font(appDensity.font(.caption2))
107 .foregroundStyle(tint) 107 .foregroundStyle(tint)
@@ -133,7 +133,7 @@ struct MonitoringLogDetailView: View {
133 LabeledContent("Alerts", value: "\(log.alertsTriggered)") 133 LabeledContent("Alerts", value: "\(log.alertsTriggered)")
134 LabeledContent("Timestamp", value: log.timestamp.formatted(date: .abbreviated, time: .shortened)) 134 LabeledContent("Timestamp", value: log.timestamp.formatted(date: .abbreviated, time: .shortened))
135 } 135 }
136 .listRowBackground(Color(.systemGray6).opacity(0.5)) 136 .listRowBackground(Color(.appSurface))
137 137
138 Section("Domains") { 138 Section("Domains") {
139 ForEach(log.checkedDomains) { result in 139 ForEach(log.checkedDomains) { result in
@@ -167,7 +167,7 @@ struct MonitoringLogDetailView: View {
167 if let errorMessage = result.errorMessage { 167 if let errorMessage = result.errorMessage {
168 Text(errorMessage) 168 Text(errorMessage)
169 .font(appDensity.font(.caption2)) 169 .font(appDensity.font(.caption2))
170 .foregroundStyle(.yellow) 170 .foregroundStyle(Color(.statusWarning))
171 } 171 }
172 172
173 if let historyEntryID = result.historyEntryID, 173 if let historyEntryID = result.historyEntryID,
@@ -181,7 +181,7 @@ struct MonitoringLogDetailView: View {
181 .padding(.vertical, 4) 181 .padding(.vertical, 4)
182 } 182 }
183 } 183 }
184 .listRowBackground(Color(.systemGray6).opacity(0.5)) 184 .listRowBackground(Color(.appSurface))
185 185
186 if !log.errors.isEmpty { 186 if !log.errors.isEmpty {
187 Section("Errors") { 187 Section("Errors") {
@@ -191,11 +191,11 @@ struct MonitoringLogDetailView: View {
191 .foregroundStyle(.secondary) 191 .foregroundStyle(.secondary)
192 } 192 }
193 } 193 }
194 .listRowBackground(Color(.systemGray6).opacity(0.5)) 194 .listRowBackground(Color(.appSurface))
195 } 195 }
196 } 196 }
197 .scrollContentBackground(.hidden) 197 .scrollContentBackground(.hidden)
198 .background(Color.black) 198 .background(Color(.appBackground))
199 .navigationTitle("Run Details") 199 .navigationTitle("Run Details")
200 .preferredColorScheme(.dark) 200 .preferredColorScheme(.dark)
201 } 201 }
@@ -205,9 +205,9 @@ struct MonitoringLogDetailView: View {
205 case .info: 205 case .info:
206 return .secondary 206 return .secondary
207 case .warning: 207 case .warning:
208 return .yellow 208 return Color(.statusWarning)
209 case .critical: 209 case .critical:
210 return .red 210 return Color(.statusCritical)
211 } 211 }
212 } 212 }
213} 213}
DomainDig/PaywallView.swift +1 −1
@@ -75,7 +75,7 @@ struct PaywallView: View {
75 if let errorMessage = purchaseService.errorMessage { 75 if let errorMessage = purchaseService.errorMessage {
76 Text(errorMessage) 76 Text(errorMessage)
77 .font(appDensity.font(.caption, design: .default)) 77 .font(appDensity.font(.caption, design: .default))
78 .foregroundStyle(.red) 78 .foregroundStyle(Color(.statusCritical))
79 } 79 }
80 } 80 }
81 81
DomainDig/SavedDomainsView.swift +3 −3
@@ -10,7 +10,7 @@ struct SavedDomainsView: View {
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(.secondary)
13 .listRowBackground(Color(.systemGray6).opacity(0.5)) 13 .listRowBackground(Color(.appSurface))
14 } else { 14 } else {
15 ForEach(viewModel.savedDomains, id: \.self) { domain in 15 ForEach(viewModel.savedDomains, id: \.self) { domain in
16 Button { 16 Button {
@@ -22,7 +22,7 @@ struct SavedDomainsView: View {
22 .font(.system(.callout, design: .monospaced)) 22 .font(.system(.callout, design: .monospaced))
23 .foregroundStyle(.primary) 23 .foregroundStyle(.primary)
24 } 24 }
25 .listRowBackground(Color(.systemGray6).opacity(0.5)) 25 .listRowBackground(Color(.appSurface))
26 } 26 }
27 .onDelete { offsets in 27 .onDelete { offsets in
28 viewModel.removeSavedDomains(at: offsets) 28 viewModel.removeSavedDomains(at: offsets)
@@ -30,7 +30,7 @@ struct SavedDomainsView: View {
30 } 30 }
31 } 31 }
32 .scrollContentBackground(.hidden) 32 .scrollContentBackground(.hidden)
33 .background(Color.black) 33 .background(Color(.appBackground))
34 .navigationTitle("Saved Domains") 34 .navigationTitle("Saved Domains")
35 .toolbar { 35 .toolbar {
36 if !viewModel.savedDomains.isEmpty { 36 if !viewModel.savedDomains.isEmpty {
DomainDig/ScheduledReportsView.swift +4 −4
@@ -67,7 +67,7 @@ struct ScheduledReportsView: View {
67 } 67 }
68 .disabled(isGenerating || !FeatureAccessService.hasAccess(to: .automatedMonitoring)) 68 .disabled(isGenerating || !FeatureAccessService.hasAccess(to: .automatedMonitoring))
69 } 69 }
70 .listRowBackground(Color(.systemGray6).opacity(0.5)) 70 .listRowBackground(Color(.appSurface))
71 71
72 if logs.isEmpty { 72 if logs.isEmpty {
73 Section { 73 Section {
@@ -79,7 +79,7 @@ struct ScheduledReportsView: View {
79 showsCardBackground: false 79 showsCardBackground: false
80 ) 80 )
81 } 81 }
82 .listRowBackground(Color(.systemGray6).opacity(0.5)) 82 .listRowBackground(Color(.appSurface))
83 } else { 83 } else {
84 Section("Recent Reports") { 84 Section("Recent Reports") {
85 ForEach(logs) { log in 85 ForEach(logs) { log in
@@ -96,11 +96,11 @@ struct ScheduledReportsView: View {
96 } 96 }
97 } 97 }
98 } 98 }
99 .listRowBackground(Color(.systemGray6).opacity(0.5)) 99 .listRowBackground(Color(.appSurface))
100 } 100 }
101 } 101 }
102 .scrollContentBackground(.hidden) 102 .scrollContentBackground(.hidden)
103 .background(Color.black) 103 .background(Color(.appBackground))
104 .navigationTitle("Scheduled Reports") 104 .navigationTitle("Scheduled Reports")
105 } 105 }
106 106
DomainDig/TimelineView.swift +5 −5
@@ -45,12 +45,12 @@ struct TimelineView: View {
45 } 45 }
46 } 46 }
47 } 47 }
48 .listRowBackground(Color(.systemGray6).opacity(0.5)) 48 .listRowBackground(Color(.appSurface))
49 } 49 }
50 } 50 }
51 } 51 }
52 .scrollContentBackground(.hidden) 52 .scrollContentBackground(.hidden)
53 .background(Color.black) 53 .background(Color(.appBackground))
54 .navigationTitle(domain) 54 .navigationTitle(domain)
55 .toolbar { 55 .toolbar {
56 ToolbarItemGroup(placement: .topBarTrailing) { 56 ToolbarItemGroup(placement: .topBarTrailing) {
@@ -116,8 +116,8 @@ private struct TimelineRow: View {
116 model: .init( 116 model: .init(
117 title: severity.title, 117 title: severity.title,
118 systemImage: "arrow.triangle.2.circlepath", 118 systemImage: "arrow.triangle.2.circlepath",
119 foregroundColor: severity == .high ? .red : .yellow, 119 foregroundColor: severity == .high ? Color(.statusCritical) : Color(.statusWarning),
120 backgroundColor: (severity == .high ? Color.red : .yellow).opacity(0.16) 120 backgroundColor: (severity == .high ? Color(.statusCritical) : Color(.statusWarning)).opacity(0.16)
121 ) 121 )
122 ) 122 )
123 } 123 }
@@ -203,7 +203,7 @@ struct TimelineDiffView: View {
203 } 203 }
204 .padding() 204 .padding()
205 } 205 }
206 .background(Color.black) 206 .background(Color(.appBackground))
207 .navigationTitle("Compare Snapshots") 207 .navigationTitle("Compare Snapshots")
208 .navigationBarTitleDisplayMode(.inline) 208 .navigationBarTitleDisplayMode(.inline)
209 .onAppear { 209 .onAppear {
DomainDig/WatchlistView.swift +23 −23
@@ -38,7 +38,7 @@ struct WatchlistView: View {
38 Section("Refresh Progress") { 38 Section("Refresh Progress") {
39 VStack(alignment: .leading, spacing: 8) { 39 VStack(alignment: .leading, spacing: 8) {
40 ProgressView(value: Double(viewModel.batchCompletedCount), total: Double(max(viewModel.batchTotalCount, 1))) 40 ProgressView(value: Double(viewModel.batchCompletedCount), total: Double(max(viewModel.batchTotalCount, 1)))
41 .tint(.cyan) 41 .tint(Color(.statusInfo))
42 HStack { 42 HStack {
43 Text(viewModel.batchProgressLabel) 43 Text(viewModel.batchProgressLabel)
44 .font(appDensity.font(.caption)) 44 .font(appDensity.font(.caption))
@@ -59,7 +59,7 @@ struct WatchlistView: View {
59 } 59 }
60 .padding(.vertical, 4) 60 .padding(.vertical, 4)
61 } 61 }
62 .listRowBackground(Color(.systemGray6).opacity(0.5)) 62 .listRowBackground(Color(.appSurface))
63 } 63 }
64 64
65 if viewModel.filteredTrackedDomains.isEmpty { 65 if viewModel.filteredTrackedDomains.isEmpty {
@@ -72,7 +72,7 @@ struct WatchlistView: View {
72 showsCardBackground: false 72 showsCardBackground: false
73 ) 73 )
74 } 74 }
75 .listRowBackground(Color(.systemGray6).opacity(0.5)) 75 .listRowBackground(Color(.appSurface))
76 } else { 76 } else {
77 if let limitMessage = FeatureAccessService.trackedDomainLimitMessage(currentCount: viewModel.trackedDomains.count) { 77 if let limitMessage = FeatureAccessService.trackedDomainLimitMessage(currentCount: viewModel.trackedDomains.count) {
78 Section { 78 Section {
@@ -80,7 +80,7 @@ struct WatchlistView: View {
80 .font(appDensity.font(.caption)) 80 .font(appDensity.font(.caption))
81 .foregroundStyle(.secondary) 81 .foregroundStyle(.secondary)
82 } 82 }
83 .listRowBackground(Color(.systemGray6).opacity(0.5)) 83 .listRowBackground(Color(.appSurface))
84 } 84 }
85 85
86 if !pinnedDomains.isEmpty { 86 if !pinnedDomains.isEmpty {
@@ -94,7 +94,7 @@ struct WatchlistView: View {
94 } 94 }
95 .animation(.easeInOut(duration: 0.2), value: viewModel.filteredTrackedDomains.map(\.id)) 95 .animation(.easeInOut(duration: 0.2), value: viewModel.filteredTrackedDomains.map(\.id))
96 .scrollContentBackground(.hidden) 96 .scrollContentBackground(.hidden)
97 .background(Color.black) 97 .background(Color(.appBackground))
98 .navigationTitle("Watchlist") 98 .navigationTitle("Watchlist")
99 .searchable(text: $viewModel.watchlistSearchText, prompt: "Search tracked domains") 99 .searchable(text: $viewModel.watchlistSearchText, prompt: "Search tracked domains")
100 .toolbar { 100 .toolbar {
@@ -215,7 +215,7 @@ struct WatchlistView: View {
215 Section { 215 Section {
216 Text(addDomainError) 216 Text(addDomainError)
217 .font(appDensity.font(.caption)) 217 .font(appDensity.font(.caption))
218 .foregroundStyle(.red) 218 .foregroundStyle(Color(.statusCritical))
219 } 219 }
220 } 220 }
221 } 221 }
@@ -317,14 +317,14 @@ struct WatchlistView: View {
317 } label: { 317 } label: {
318 Label("Refresh", systemImage: "arrow.clockwise") 318 Label("Refresh", systemImage: "arrow.clockwise")
319 } 319 }
320 .tint(.cyan) 320 .tint(Color(.statusInfo))
321 321
322 Button { 322 Button {
323 viewModel.togglePinned(for: trackedDomain) 323 viewModel.togglePinned(for: trackedDomain)
324 } label: { 324 } label: {
325 Label(trackedDomain.isPinned ? "Unpin" : "Pin", systemImage: trackedDomain.isPinned ? "pin.slash" : "pin") 325 Label(trackedDomain.isPinned ? "Unpin" : "Pin", systemImage: trackedDomain.isPinned ? "pin.slash" : "pin")
326 } 326 }
327 .tint(.yellow) 327 .tint(Color(.statusWarning))
328 } 328 }
329 .swipeActions(edge: .trailing, allowsFullSwipe: false) { 329 .swipeActions(edge: .trailing, allowsFullSwipe: false) {
330 Button { 330 Button {
@@ -333,7 +333,7 @@ struct WatchlistView: View {
333 } label: { 333 } label: {
334 Label("Refresh", systemImage: "arrow.clockwise") 334 Label("Refresh", systemImage: "arrow.clockwise")
335 } 335 }
336 .tint(.cyan) 336 .tint(Color(.statusInfo))
337 337
338 if viewModel.canDelete(trackedDomain) { 338 if viewModel.canDelete(trackedDomain) {
339 Button(role: .destructive) { 339 Button(role: .destructive) {
@@ -380,7 +380,7 @@ struct WatchlistView: View {
380 } 380 }
381 } 381 }
382 } 382 }
383 .listRowBackground(Color(.systemGray6).opacity(0.5)) 383 .listRowBackground(Color(.appSurface))
384 } 384 }
385 385
386 private var batchSummaryBinding: Binding<BatchSweepSummary?> { 386 private var batchSummaryBinding: Binding<BatchSweepSummary?> {
@@ -437,7 +437,7 @@ struct WatchlistRowView: View {
437 if trackedDomain.isPinned { 437 if trackedDomain.isPinned {
438 Image(systemName: "pin.fill") 438 Image(systemName: "pin.fill")
439 .font(.caption2) 439 .font(.caption2)
440 .foregroundStyle(.yellow) 440 .foregroundStyle(Color(.statusWarning))
441 } 441 }
442 Text(trackedDomain.domain) 442 Text(trackedDomain.domain)
443 .font(appDensity.font(.callout)) 443 .font(appDensity.font(.callout))
@@ -502,7 +502,7 @@ struct WatchlistRowView: View {
502 @ViewBuilder 502 @ViewBuilder
503 private var statusBadge: some View { 503 private var statusBadge: some View {
504 if isRefreshing { 504 if isRefreshing {
505 AppStatusBadgeView(model: .init(title: "Refreshing", systemImage: "arrow.clockwise", foregroundColor: .secondary, backgroundColor: Color(.systemGray5).opacity(0.6))) 505 AppStatusBadgeView(model: .init(title: "Refreshing", systemImage: "arrow.clockwise", foregroundColor: .secondary, backgroundColor: Color(.appSurfaceElevated)))
506 } else { 506 } else {
507 AppStatusBadgeView(model: AppStatusFactory.availability(trackedDomain.lastKnownAvailability)) 507 AppStatusBadgeView(model: AppStatusFactory.availability(trackedDomain.lastKnownAvailability))
508 } 508 }
@@ -516,8 +516,8 @@ struct WatchlistRowView: View {
516 model: .init( 516 model: .init(
517 title: "Shared", 517 title: "Shared",
518 systemImage: "person.2.fill", 518 systemImage: "person.2.fill",
519 foregroundColor: .cyan, 519 foregroundColor: Color(.statusInfo),
520 backgroundColor: .cyan.opacity(0.16) 520 backgroundColor: Color(.statusInfo).opacity(0.16)
521 ) 521 )
522 ) 522 )
523 } 523 }
@@ -534,11 +534,11 @@ struct WatchlistRowView: View {
534 let days = trackedDomain.certificateDaysRemaining.map { "\($0)d" } ?? "Soon" 534 let days = trackedDomain.certificateDaysRemaining.map { "\($0)d" } ?? "Soon"
535 switch trackedDomain.certificateWarningLevel { 535 switch trackedDomain.certificateWarningLevel {
536 case .critical: 536 case .critical:
537 return .init(title: "Invalid \(days)", systemImage: "xmark.octagon.fill", foregroundColor: .red, backgroundColor: .red.opacity(0.16)) 537 return .init(title: "Invalid \(days)", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
538 case .warning: 538 case .warning:
539 return .init(title: "Expiring \(days)", systemImage: "exclamationmark.triangle.fill", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16)) 539 return .init(title: "Expiring \(days)", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
540 case .none: 540 case .none:
541 return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16)) 541 return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
542 } 542 }
543 } 543 }
544} 544}
@@ -577,7 +577,7 @@ struct TrackedDomainDetailView: View {
577 isRefreshing: viewModel.refreshingTrackedDomainID == liveTrackedDomain.id 577 isRefreshing: viewModel.refreshingTrackedDomainID == liveTrackedDomain.id
578 ) 578 )
579 } 579 }
580 .listRowBackground(Color(.systemGray6).opacity(0.5)) 580 .listRowBackground(Color(.appSurface))
581 581
582 Section { 582 Section {
583 Button { 583 Button {
@@ -652,13 +652,13 @@ struct TrackedDomainDetailView: View {
652 Label(liveTrackedDomain.collaboration?.isShared == true ? "Manage Share" : "Share Domain", systemImage: "person.2") 652 Label(liveTrackedDomain.collaboration?.isShared == true ? "Manage Share" : "Share Domain", systemImage: "person.2")
653 } 653 }
654 } 654 }
655 .listRowBackground(Color(.systemGray6).opacity(0.5)) 655 .listRowBackground(Color(.appSurface))
656 656
657 if !liveTrackedDomain.tags.isEmpty { 657 if !liveTrackedDomain.tags.isEmpty {
658 Section("Tags") { 658 Section("Tags") {
659 TagChipRowView(tags: liveTrackedDomain.tags) 659 TagChipRowView(tags: liveTrackedDomain.tags)
660 } 660 }
661 .listRowBackground(Color(.systemGray6).opacity(0.5)) 661 .listRowBackground(Color(.appSurface))
662 } 662 }
663 663
664 Section("Monitoring Status") { 664 Section("Monitoring Status") {
@@ -672,7 +672,7 @@ struct TrackedDomainDetailView: View {
672 LabeledContent("Queued Alerts", value: "\(liveTrackedDomain.pendingMonitoringAlerts.count)") 672 LabeledContent("Queued Alerts", value: "\(liveTrackedDomain.pendingMonitoringAlerts.count)")
673 } 673 }
674 } 674 }
675 .listRowBackground(Color(.systemGray6).opacity(0.5)) 675 .listRowBackground(Color(.appSurface))
676 676
677 if let summary = viewModel.latestChangeSummary(for: liveTrackedDomain) { 677 if let summary = viewModel.latestChangeSummary(for: liveTrackedDomain) {
678 Section("Latest Change Summary") { 678 Section("Latest Change Summary") {
@@ -718,10 +718,10 @@ struct TrackedDomainDetailView: View {
718 } 718 }
719 } 719 }
720 } 720 }
721 .listRowBackground(Color(.systemGray6).opacity(0.5)) 721 .listRowBackground(Color(.appSurface))
722 } 722 }
723 .scrollContentBackground(.hidden) 723 .scrollContentBackground(.hidden)
724 .background(Color.black) 724 .background(Color(.appBackground))
725 .navigationTitle(liveTrackedDomain.domain) 725 .navigationTitle(liveTrackedDomain.domain)
726 .preferredColorScheme(.dark) 726 .preferredColorScheme(.dark)
727 .onChange(of: viewModel.rerunNavigationToken) { _, _ in 727 .onChange(of: viewModel.rerunNavigationToken) { _, _ in
DomainDig/WorkflowsView.swift +9 −9
@@ -24,13 +24,13 @@ struct WorkflowsView: View {
24 .font(appDensity.font(.caption)) 24 .font(appDensity.font(.caption))
25 .foregroundStyle(.secondary) 25 .foregroundStyle(.secondary)
26 } 26 }
27 .listRowBackground(Color(.systemGray6).opacity(0.5)) 27 .listRowBackground(Color(.appSurface))
28 } 28 }
29 29
30 workflowContent 30 workflowContent
31 } 31 }
32 .scrollContentBackground(.hidden) 32 .scrollContentBackground(.hidden)
33 .background(Color.black) 33 .background(Color(.appBackground))
34 .refreshable { 34 .refreshable {
35 await viewModel.refreshWorkflowList() 35 await viewModel.refreshWorkflowList()
36 } 36 }
@@ -73,7 +73,7 @@ struct WorkflowsView: View {
73 value: Double(viewModel.batchCompletedCount), 73 value: Double(viewModel.batchCompletedCount),
74 total: Double(max(viewModel.batchTotalCount, 1)) 74 total: Double(max(viewModel.batchTotalCount, 1))
75 ) 75 )
76 .tint(.cyan) 76 .tint(Color(.statusInfo))
77 77
78 HStack { 78 HStack {
79 Text(viewModel.batchProgressLabel) 79 Text(viewModel.batchProgressLabel)
@@ -91,7 +91,7 @@ struct WorkflowsView: View {
91 } 91 }
92 .padding(.vertical, 4) 92 .padding(.vertical, 4)
93 } 93 }
94 .listRowBackground(Color(.systemGray6).opacity(0.5)) 94 .listRowBackground(Color(.appSurface))
95 } 95 }
96 96
97 if viewModel.workflows.isEmpty { 97 if viewModel.workflows.isEmpty {
@@ -104,7 +104,7 @@ struct WorkflowsView: View {
104 showsCardBackground: false 104 showsCardBackground: false
105 ) 105 )
106 } 106 }
107 .listRowBackground(Color(.systemGray6).opacity(0.5)) 107 .listRowBackground(Color(.appSurface))
108 } else { 108 } else {
109 ForEach(viewModel.workflows) { workflow in 109 ForEach(viewModel.workflows) { workflow in
110 NavigationLink { 110 NavigationLink {
@@ -112,7 +112,7 @@ struct WorkflowsView: View {
112 } label: { 112 } label: {
113 WorkflowRowView(workflow: workflow) 113 WorkflowRowView(workflow: workflow)
114 } 114 }
115 .listRowBackground(Color(.systemGray6).opacity(0.5)) 115 .listRowBackground(Color(.appSurface))
116 } 116 }
117 .onDelete { offsets in 117 .onDelete { offsets in
118 let workflows = offsets.map { viewModel.workflows[$0] } 118 let workflows = offsets.map { viewModel.workflows[$0] }
@@ -133,7 +133,7 @@ private struct WorkflowRowView: View {
133 if workflow.collaboration?.isShared == true { 133 if workflow.collaboration?.isShared == true {
134 Image(systemName: "person.2.fill") 134 Image(systemName: "person.2.fill")
135 .font(.caption2) 135 .font(.caption2)
136 .foregroundStyle(.cyan) 136 .foregroundStyle(Color(.statusInfo))
137 } 137 }
138 Text(workflow.name) 138 Text(workflow.name)
139 .font(appDensity.font(.callout, design: .default, weight: .semibold)) 139 .font(appDensity.font(.callout, design: .default, weight: .semibold))
@@ -335,7 +335,7 @@ struct WorkflowDetailView: View {
335 } 335 }
336 } 336 }
337 .scrollContentBackground(.hidden) 337 .scrollContentBackground(.hidden)
338 .background(Color.black) 338 .background(Color(.appBackground))
339 .navigationTitle(workflow.name) 339 .navigationTitle(workflow.name)
340 .toolbar { 340 .toolbar {
341 if !workflow.domains.isEmpty { 341 if !workflow.domains.isEmpty {
@@ -640,7 +640,7 @@ struct WorkflowBulkAddSheet: View {
640 .foregroundStyle(.primary) 640 .foregroundStyle(.primary)
641 Spacer() 641 Spacer()
642 Image(systemName: selectedDomains.contains(domain) ? "checkmark.circle.fill" : "circle") 642 Image(systemName: selectedDomains.contains(domain) ? "checkmark.circle.fill" : "circle")
643 .foregroundStyle(selectedDomains.contains(domain) ? .cyan : .secondary) 643 .foregroundStyle(selectedDomains.contains(domain) ? Color(.statusInfo) : .secondary)
644 } 644 }
645 } 645 }
646 .buttonStyle(.plain) 646 .buttonStyle(.plain)
DomainDigUITests/AccessibilityAuditHarness.swift +7 −1
@@ -87,7 +87,13 @@ enum AccessibilityAuditHarness {
87 try app.performAccessibilityAudit { issue in 87 try app.performAccessibilityAudit { issue in
88 let isEnforced = !enforcedAuditTypes.intersection(issue.auditType).isEmpty 88 let isEnforced = !enforcedAuditTypes.intersection(issue.auditType).isEmpty
89 let marker = isEnforced ? "FAIL" : "report" 89 let marker = isEnforced ? "FAIL" : "report"
90 findings.append("[\(marker)][\(name(for: issue.auditType))] \(issue.compactDescription)") 90 // Include the element so the burndown says *what* to fix, not
91 // just that something is wrong.
92 let element = issue.element.map { el -> String in
93 let label = el.label.isEmpty ? el.identifier : el.label
94 return label.isEmpty ? "\(el.elementType)" : "\"\(label)\""
95 } ?? "unknown element"
96 findings.append("[\(marker)][\(name(for: issue.auditType))] \(issue.compactDescription) — \(element)")
91 // true suppresses the finding, false reports it as a test failure. 97 // true suppresses the finding, false reports it as a test failure.
92 return !isEnforced 98 return !isEnforced
93 } 99 }
DomainDigUITests/AccessibilityAuditTests.swift +14 −1
@@ -17,7 +17,20 @@ final class AccessibilityAuditTests: XCTestCase {
17 // MARK: Per-screen audits 17 // MARK: Per-screen audits
18 18
19 func testInspectScreen() throws { 19 func testInspectScreen() throws {
20 try auditRootTab("Inspect") 20 let app = AccessibilityAuditHarness.launch()
21 app.selectRootTab("Inspect")
22
23 // Type a domain so the Run button is enabled. A disabled control has no
24 // contrast requirement under WCAG 1.4.3, but the audit still flags it,
25 // so auditing the empty state would report a false positive forever.
26 let field = app.textFields.firstMatch
27 if field.waitForExistence(timeout: 5) {
28 field.tap()
29 field.typeText("example.com")
30 }
31
32 let audited = try AccessibilityAuditHarness.audit(app, screen: "inspect", test: self)
33 try XCTSkipUnless(audited, "Audit did not complete in time for Inspect")
21 } 34 }
22 35
23 func testDashboardScreen() throws { 36 func testDashboardScreen() throws {
DomainDigWidget/DomainDigPortfolioWidget.swift +10 −10
@@ -91,9 +91,9 @@ struct DomainDigWidgetView: View {
91 Spacer(minLength: 0) 91 Spacer(minLength: 0)
92 92
93 HStack(spacing: 10) { 93 HStack(spacing: 10) {
94 countPill(data.healthyCount, .green) 94 countPill(data.healthyCount, Color(.statusPositive))
95 countPill(data.warningCount, .orange) 95 countPill(data.warningCount, Color(.statusWarning))
96 countPill(data.criticalCount, .red) 96 countPill(data.criticalCount, Color(.statusCritical))
97 } 97 }
98 } 98 }
99 } 99 }
@@ -121,10 +121,10 @@ struct DomainDigWidgetView: View {
121 } 121 }
122 122
123 HStack(spacing: 12) { 123 HStack(spacing: 12) {
124 summaryStat(data.healthyCount, "Healthy", .green) 124 summaryStat(data.healthyCount, "Healthy", Color(.statusPositive))
125 summaryStat(data.warningCount, "Warning", .orange) 125 summaryStat(data.warningCount, "Warning", Color(.statusWarning))
126 summaryStat(data.criticalCount, "Critical", .red) 126 summaryStat(data.criticalCount, "Critical", Color(.statusCritical))
127 summaryStat(data.expiringSoonCount, "Expiring", .yellow) 127 summaryStat(data.expiringSoonCount, "Expiring", Color(.statusWarning))
128 } 128 }
129 129
130 Divider() 130 Divider()
@@ -180,9 +180,9 @@ struct DomainDigWidgetView: View {
180 180
181 private func color(for status: DomainDigWidgetStatus) -> Color { 181 private func color(for status: DomainDigWidgetStatus) -> Color {
182 switch status { 182 switch status {
183 case .healthy: return .green 183 case .healthy: return Color(.statusPositive)
184 case .warning: return .orange 184 case .warning: return Color(.statusWarning)
185 case .critical: return .red 185 case .critical: return Color(.statusCritical)
186 } 186 }
187 } 187 }
188} 188}
DomainDigWidget/SweepLiveActivity.swift +6 −6
@@ -24,7 +24,7 @@ struct SweepLiveActivity: Widget {
24 DynamicIslandExpandedRegion(.bottom) { 24 DynamicIslandExpandedRegion(.bottom) {
25 VStack(alignment: .leading, spacing: 4) { 25 VStack(alignment: .leading, spacing: 4) {
26 ProgressView(value: context.state.fractionComplete) 26 ProgressView(value: context.state.fractionComplete)
27 .tint(.cyan) 27 .tint(Color(.statusInfo))
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)
@@ -35,7 +35,7 @@ struct SweepLiveActivity: Widget {
35 } 35 }
36 } compactLeading: { 36 } compactLeading: {
37 Image(systemName: "arrow.trianglehead.2.clockwise") 37 Image(systemName: "arrow.trianglehead.2.clockwise")
38 .foregroundStyle(.cyan) 38 .foregroundStyle(Color(.statusInfo))
39 } compactTrailing: { 39 } compactTrailing: {
40 Text("\(context.state.completed)/\(context.state.total)") 40 Text("\(context.state.completed)/\(context.state.total)")
41 .font(.caption2) 41 .font(.caption2)
@@ -43,7 +43,7 @@ struct SweepLiveActivity: Widget {
43 } minimal: { 43 } minimal: {
44 ProgressView(value: context.state.fractionComplete) 44 ProgressView(value: context.state.fractionComplete)
45 .progressViewStyle(.circular) 45 .progressViewStyle(.circular)
46 .tint(.cyan) 46 .tint(Color(.statusInfo))
47 } 47 }
48 } 48 }
49 } 49 }
@@ -66,7 +66,7 @@ private struct SweepActivityBannerView: View {
66 } 66 }
67 67
68 ProgressView(value: context.state.fractionComplete) 68 ProgressView(value: context.state.fractionComplete)
69 .tint(.cyan) 69 .tint(Color(.statusInfo))
70 70
71 HStack { 71 HStack {
72 if let current = context.state.currentDomain { 72 if let current = context.state.currentDomain {
@@ -83,12 +83,12 @@ private struct SweepActivityBannerView: View {
83 if context.state.changed > 0 { 83 if context.state.changed > 0 {
84 Text("\(context.state.changed) changed") 84 Text("\(context.state.changed) changed")
85 .font(.caption2) 85 .font(.caption2)
86 .foregroundStyle(.orange) 86 .foregroundStyle(Color(.statusWarning))
87 } 87 }
88 if context.state.warnings > 0 { 88 if context.state.warnings > 0 {
89 Text("\(context.state.warnings) warnings") 89 Text("\(context.state.warnings) warnings")
90 .font(.caption2) 90 .font(.caption2)
91 .foregroundStyle(.red) 91 .foregroundStyle(Color(.statusCritical))
92 } 92 }
93 } 93 }
94 } 94 }
Shared/Colors.xcassets/AccentFill.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": "0x00",
9 "green": "0x00",
10 "blue": "0xFF",
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": "0x00",
27 "green": "0x40",
28 "blue": "0xDD",
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": "0x00",
45 "green": "0x00",
46 "blue": "0xCC",
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": "0x00",
67 "green": "0x33",
68 "blue": "0xCC",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}
Shared/Colors.xcassets/AppBackground.colorset/Contents.json added +38
@@ -0,0 +1,38 @@
1{
2 "colors": [
3 {
4 "idiom": "universal",
5 "color": {
6 "color-space": "srgb",
7 "components": {
8 "red": "0xFF",
9 "green": "0xFF",
10 "blue": "0xFF",
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": "0x00",
27 "green": "0x00",
28 "blue": "0x00",
29 "alpha": "1.000"
30 }
31 }
32 }
33 ],
34 "info": {
35 "author": "xcode",
36 "version": 1
37 }
38}
Shared/Colors.xcassets/AppOnAccent.colorset/Contents.json added +38
@@ -0,0 +1,38 @@
1{
2 "colors": [
3 {
4 "idiom": "universal",
5 "color": {
6 "color-space": "srgb",
7 "components": {
8 "red": "0xFF",
9 "green": "0xFF",
10 "blue": "0xFF",
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": "0x00",
27 "green": "0x00",
28 "blue": "0x00",
29 "alpha": "1.000"
30 }
31 }
32 }
33 ],
34 "info": {
35 "author": "xcode",
36 "version": 1
37 }
38}
Shared/Colors.xcassets/AppSeparator.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": "0xC6",
9 "green": "0xC6",
10 "blue": "0xC8",
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": "0x38",
27 "green": "0x38",
28 "blue": "0x3A",
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": "0xA0",
45 "green": "0xA0",
46 "blue": "0xA3",
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": "0x54",
67 "green": "0x54",
68 "blue": "0x56",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}
Shared/Colors.xcassets/AppSurface.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": "0xF2",
9 "green": "0xF2",
10 "blue": "0xF7",
11 "alpha": "1.000"
12 }
13 }
14 },
15 {
16 "idiom": "universal",
17 "appearances": [
18 {
19 "appearance": "luminosity",
20 "value": "dark"
21 }
22 ],
23 "color": {
24 "color-space": "srgb",
25 "components": {
26 "red": "0x0E",
27 "green": "0x0E",
28 "blue": "0x0F",
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": "0xEB",
45 "green": "0xEB",
46 "blue": "0xF0",
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": "0x16",
67 "green": "0x16",
68 "blue": "0x18",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}
Shared/Colors.xcassets/AppSurfaceElevated.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": "0xE5",
9 "green": "0xE5",
10 "blue": "0xEA",
11 "alpha": "1.000"
12 }
13 }
14 },
15 {
16 "idiom": "universal",
17 "appearances": [
18 {
19 "appearance": "luminosity",
20 "value": "dark"
21 }
22 ],
23 "color": {
24 "color-space": "srgb",
25 "components": {
26 "red": "0x1A",
27 "green": "0x1A",
28 "blue": "0x1C",
29 "alpha": "1.000"
30 }
31 }
32 },
33 {
34 "idiom": "universal",
35 "appearances": [
36 {
37 "appearance": "contrast",
38 "value": "high"
39 }
40 ],
41 "color": {
42 "color-space": "srgb",
43 "components": {
44 "red": "0xDC",
45 "green": "0xDC",
46 "blue": "0xE1",
47 "alpha": "1.000"
48 }
49 }
50 },
51 {
52 "idiom": "universal",
53 "appearances": [
54 {
55 "appearance": "luminosity",
56 "value": "dark"
57 },
58 {
59 "appearance": "contrast",
60 "value": "high"
61 }
62 ],
63 "color": {
64 "color-space": "srgb",
65 "components": {
66 "red": "0x23",
67 "green": "0x23",
68 "blue": "0x26",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}
Shared/Colors.xcassets/Contents.json added +6
@@ -0,0 +1,6 @@
1{
2 "info" : {
3 "author" : "xcode",
4 "version" : 1
5 }
6}
Shared/Colors.xcassets/StatusCritical.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": "0xB3",
9 "green": "0x26",
10 "blue": "0x1E",
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": "0xFF",
27 "green": "0x69",
28 "blue": "0x61",
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": "0x8C",
45 "green": "0x1D",
46 "blue": "0x16",
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": "0xFF",
67 "green": "0x9A",
68 "blue": "0x94",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}
Shared/Colors.xcassets/StatusInfo.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": "0x00",
9 "green": "0x00",
10 "blue": "0xFF",
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": "0x4D",
27 "green": "0xA3",
28 "blue": "0xFF",
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": "0x00",
45 "green": "0x00",
46 "blue": "0xCC",
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": "0x7F",
67 "green": "0xBF",
68 "blue": "0xFF",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}
Shared/Colors.xcassets/StatusNeutral.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}
Shared/Colors.xcassets/StatusPositive.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": "0x14",
9 "green": "0x6C",
10 "blue": "0x2E",
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": "0x30",
27 "green": "0xD1",
28 "blue": "0x58",
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": "0x0B",
45 "green": "0x50",
46 "blue": "0x23",
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": "0x5B",
67 "green": "0xE5",
68 "blue": "0x84",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}
Shared/Colors.xcassets/StatusWarning.colorset/Contents.json added +78
@@ -0,0 +1,78 @@
1{
2 "colors": [
3 {
4 "idiom": "universal",
5 "color": {
6 "color-space": "srgb",
7 "components": {
8 "red": "0x7A",
9 "green": "0x56",
10 "blue": "0x00",
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": "0xFF",
27 "green": "0xD6",
28 "blue": "0x0A",
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": "0x5C",
45 "green": "0x41",
46 "blue": "0x00",
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": "0xFF",
67 "green": "0xE5",
68 "blue": "0x66",
69 "alpha": "1.000"
70 }
71 }
72 }
73 ],
74 "info": {
75 "author": "xcode",
76 "version": 1
77 }
78}