Showing 4 changed files with 413 additions and 57 deletions
+79 -0
HealthProbe.xcodeproj/xcshareddata/xcschemes/HealthProbe.xcscheme
@@ -0,0 +1,79 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<Scheme
3
+   LastUpgradeVersion = "2640"
4
+   version = "1.7">
5
+   <BuildAction
6
+      parallelizeBuildables = "YES"
7
+      buildImplicitDependencies = "YES"
8
+      buildArchitectures = "Automatic">
9
+      <BuildActionEntries>
10
+         <BuildActionEntry
11
+            buildForTesting = "YES"
12
+            buildForRunning = "YES"
13
+            buildForProfiling = "YES"
14
+            buildForArchiving = "YES"
15
+            buildForAnalyzing = "YES">
16
+            <BuildableReference
17
+               BuildableIdentifier = "primary"
18
+               BlueprintIdentifier = "439832782FA4933E003C0182"
19
+               BuildableName = "HealthProbe.app"
20
+               BlueprintName = "HealthProbe"
21
+               ReferencedContainer = "container:HealthProbe.xcodeproj">
22
+            </BuildableReference>
23
+         </BuildActionEntry>
24
+      </BuildActionEntries>
25
+   </BuildAction>
26
+   <TestAction
27
+      buildConfiguration = "Debug"
28
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
29
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
30
+      shouldUseLaunchSchemeArgsEnv = "YES"
31
+      shouldAutocreateTestPlan = "YES">
32
+   </TestAction>
33
+   <LaunchAction
34
+      buildConfiguration = "Debug"
35
+      selectedDebuggerIdentifier = ""
36
+      selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
37
+      launchStyle = "0"
38
+      useCustomWorkingDirectory = "NO"
39
+      ignoresPersistentStateOnLaunch = "NO"
40
+      debugDocumentVersioning = "YES"
41
+      debugServiceExtension = "internal"
42
+      allowLocationSimulation = "YES"
43
+      queueDebuggingEnableBacktraceRecording = "Yes">
44
+      <BuildableProductRunnable
45
+         runnableDebuggingMode = "0">
46
+         <BuildableReference
47
+            BuildableIdentifier = "primary"
48
+            BlueprintIdentifier = "439832782FA4933E003C0182"
49
+            BuildableName = "HealthProbe.app"
50
+            BlueprintName = "HealthProbe"
51
+            ReferencedContainer = "container:HealthProbe.xcodeproj">
52
+         </BuildableReference>
53
+      </BuildableProductRunnable>
54
+   </LaunchAction>
55
+   <ProfileAction
56
+      buildConfiguration = "Release"
57
+      shouldUseLaunchSchemeArgsEnv = "YES"
58
+      savedToolIdentifier = ""
59
+      useCustomWorkingDirectory = "NO"
60
+      debugDocumentVersioning = "YES">
61
+      <BuildableProductRunnable
62
+         runnableDebuggingMode = "0">
63
+         <BuildableReference
64
+            BuildableIdentifier = "primary"
65
+            BlueprintIdentifier = "439832782FA4933E003C0182"
66
+            BuildableName = "HealthProbe.app"
67
+            BlueprintName = "HealthProbe"
68
+            ReferencedContainer = "container:HealthProbe.xcodeproj">
69
+         </BuildableReference>
70
+      </BuildableProductRunnable>
71
+   </ProfileAction>
72
+   <AnalyzeAction
73
+      buildConfiguration = "Debug">
74
+   </AnalyzeAction>
75
+   <ArchiveAction
76
+      buildConfiguration = "Release"
77
+      revealArchiveInOrganizer = "YES">
78
+   </ArchiveAction>
79
+</Scheme>
+8 -0
HealthProbe.xcodeproj/xcuserdata/bogdan.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -10,5 +10,13 @@
10 10
 			<integer>0</integer>
11 11
 		</dict>
12 12
 	</dict>
13
+	<key>SuppressBuildableAutocreation</key>
14
+	<dict>
15
+		<key>439832782FA4933E003C0182</key>
16
+		<dict>
17
+			<key>primary</key>
18
+			<true/>
19
+		</dict>
20
+	</dict>
13 21
 </dict>
14 22
 </plist>
+1 -1
HealthProbe/Utilities/DesignSystem.swift
@@ -51,7 +51,7 @@ struct SeverityBadge: View {
51 51
 
52 52
     private var badge: (String, Color) {
53 53
         if delta > 0 { return ("+\(delta)", .healthyGreen) }
54
-        if delta < 0 { return ("\(delta)", .criticalRed) }
54
+        if delta < 0 { return ("\(delta)", .neutralGray) }
55 55
         return ("–", .neutralGray)
56 56
     }
57 57
 }
+325 -56
HealthProbe/Views/Snapshots/SnapshotDetailView.swift
@@ -38,6 +38,20 @@ struct SnapshotDetailView: View {
38 38
         }
39 39
     }
40 40
 
41
+    private var snapshotEarliestRecordDate: Date? {
42
+        sortedTypeCounts
43
+            .filter { !$0.isUnsupported && $0.count > 0 }
44
+            .compactMap(\.earliestDate)
45
+            .min()
46
+    }
47
+
48
+    private var snapshotNewestRecordDate: Date? {
49
+        sortedTypeCounts
50
+            .filter { !$0.isUnsupported && $0.count > 0 }
51
+            .compactMap(\.latestDate)
52
+            .max()
53
+    }
54
+
41 55
     private var deviceDisplayName: String {
42 56
         if let name = profile?.name, !name.isEmpty { return name }
43 57
         return currentSnapshot.deviceName.isEmpty ? "Unknown device" : currentSnapshot.deviceName
@@ -140,14 +154,10 @@ struct SnapshotDetailView: View {
140 154
     @State private var showShareSheet = false
141 155
     @State private var pdfExportURL: URL?
142 156
     @State private var isExporting = false
157
+    @State private var showMetadataSheet = false
143 158
 
144 159
     var body: some View {
145 160
         List {
146
-            summarySection
147
-            deviceSection
148
-            if let baseline {
149
-                comparisonSection(baseline)
150
-            }
151 161
             evolutionSection
152 162
         }
153 163
         .navigationTitle("Snapshot")
@@ -161,19 +171,31 @@ struct SnapshotDetailView: View {
161 171
                 snapshotToolbarTitle
162 172
             }
163 173
             ToolbarItem(placement: .navigationBarTrailing) {
164
-                if isExporting {
165
-                    ProgressView()
166
-                        .accessibilityLabel("Generating PDF")
167
-                } else {
174
+                HStack(spacing: 12) {
168 175
                     Button {
169
-                        exportAsPDF()
176
+                        showMetadataSheet = true
170 177
                     } label: {
171
-                        Image(systemName: "square.and.arrow.up")
178
+                        Image(systemName: "info.circle")
179
+                    }
180
+                    .accessibilityLabel("View snapshot details")
181
+
182
+                    if isExporting {
183
+                        ProgressView()
184
+                            .accessibilityLabel("Generating PDF")
185
+                    } else {
186
+                        Button {
187
+                            exportAsPDF()
188
+                        } label: {
189
+                            Image(systemName: "square.and.arrow.up")
190
+                        }
191
+                        .accessibilityLabel("Export snapshot as PDF")
172 192
                     }
173
-                    .accessibilityLabel("Export snapshot as PDF")
174 193
                 }
175 194
             }
176 195
         }
196
+        .sheet(isPresented: $showMetadataSheet) {
197
+            metadataSheetContent
198
+        }
177 199
         .sheet(isPresented: $showShareSheet) {
178 200
             if let url = pdfExportURL {
179 201
                 ShareSheet(items: [url])
@@ -216,7 +238,7 @@ struct SnapshotDetailView: View {
216 238
                     in: Capsule()
217 239
                 )
218 240
         } else {
219
-            Text("Snapshot")
241
+            Text(currentSnapshot.timestamp, format: .dateTime.month().day().hour().minute())
220 242
                 .font(.headline.weight(.semibold))
221 243
                 .padding(.horizontal, 18)
222 244
                 .frame(height: 36)
@@ -283,11 +305,8 @@ struct SnapshotDetailView: View {
283 305
                 }
284 306
             } label: {
285 307
                 VStack(spacing: 2) {
286
-                    Text("Snapshot")
287
-                        .font(.headline.weight(.semibold))
288 308
                     Text(currentSnapshot.timestamp, format: .dateTime.month().day().hour().minute())
289
-                        .font(.caption)
290
-                        .foregroundStyle(.secondary)
309
+                        .font(.headline.weight(.semibold))
291 310
                 }
292 311
             }
293 312
             .buttonStyle(.plain)
@@ -336,64 +355,178 @@ struct SnapshotDetailView: View {
336 355
         }
337 356
     }
338 357
 
339
-    private var summarySection: some View {
340
-        Section("Summary") {
341
-            DetailRow(label: "Captured") {
342
-                Text(currentSnapshot.timestamp, format: .dateTime.year().month().day().hour().minute())
343
-                    .foregroundStyle(.secondary)
344
-            }
345
-            DetailRow(label: "Tracked Types") {
346
-                Text("\(sortedTypeCounts.count)")
347
-                    .foregroundStyle(.secondary)
348
-            }
349
-            DetailRow(label: "Total Records") {
350
-                Text("\(totalCount)")
351
-                    .foregroundStyle(.secondary)
352
-                    .monospacedDigit()
358
+    @ViewBuilder
359
+    private var metadataSheetContent: some View {
360
+        NavigationStack {
361
+            ScrollView {
362
+                VStack(alignment: .leading, spacing: 12) {
363
+                    // Title with Date
364
+                    VStack(spacing: 4) {
365
+                        Text("Snapshot")
366
+                            .font(.headline.weight(.semibold))
367
+                        Text(currentSnapshot.timestamp, format: .dateTime.month().day().hour().minute())
368
+                            .font(.subheadline)
369
+                            .foregroundStyle(.secondary)
370
+                    }
371
+                    .frame(maxWidth: .infinity, alignment: .center)
372
+                    .padding(12)
373
+                    .background(Color(.systemBackground).opacity(0.5), in: RoundedRectangle(cornerRadius: 12))
374
+
375
+                    // Data Range
376
+                    SnapshotDataRangeIndicator(
377
+                        oldestRecordDate: snapshotEarliestRecordDate,
378
+                        newestRecordDate: snapshotNewestRecordDate,
379
+                        quality: currentSnapshot.snapshotQuality
380
+                    )
381
+
382
+                    // Summary Stats (compact)
383
+                    VStack(spacing: 12) {
384
+                        HStack(spacing: 16) {
385
+                            statCompact(label: "Types", value: "\(sortedTypeCounts.count)")
386
+                            Divider()
387
+                            statCompact(label: "Records", value: "\(totalCount)")
388
+                        }
389
+                        .font(.caption)
390
+                        .foregroundStyle(.secondary)
391
+                    }
392
+                    .padding(12)
393
+                    .background(Color(.systemBackground).opacity(0.5), in: RoundedRectangle(cornerRadius: 12))
394
+
395
+                    // Device (collapsible)
396
+                    DisclosureGroup {
397
+                        VStack(alignment: .leading, spacing: 12) {
398
+                            DetailRow(label: "Version") {
399
+                                Text(extractOSVersion(currentSnapshot.osVersion))
400
+                                    .foregroundStyle(.secondary)
401
+                                    .font(.caption.monospacedDigit())
402
+                            }
403
+                            Divider()
404
+                            DetailRow(label: "Build") {
405
+                                Text(extractBuildNumber(currentSnapshot.osVersion))
406
+                                    .foregroundStyle(.secondary)
407
+                                    .font(.caption.monospacedDigit())
408
+                            }
409
+                        }
410
+                        .padding(.top, 8)
411
+                    } label: {
412
+                        HStack(spacing: 8) {
413
+                            Image(systemName: "iphone")
414
+                                .font(.system(size: 16, weight: .semibold))
415
+                            Text(deviceDisplayName)
416
+                                .font(.subheadline.weight(.semibold))
417
+                        }
418
+                    }
419
+                    .padding(12)
420
+                    .background(Color(.systemBackground).opacity(0.5), in: RoundedRectangle(cornerRadius: 12))
421
+
422
+                    // Comparison (if baseline exists)
423
+                    if let baseline {
424
+                        comparisonSection(baseline: baseline)
425
+                    }
426
+
427
+                    Spacer()
428
+                }
429
+                .padding(16)
353 430
             }
431
+            .navigationTitle("Snapshot")
432
+            .navigationBarTitleDisplayMode(.inline)
354 433
         }
355 434
     }
356 435
 
357
-    private var deviceSection: some View {
358
-        Section("Device") {
359
-            DetailRow(label: "Name") {
360
-                Text(deviceDisplayName)
361
-                    .foregroundStyle(.secondary)
436
+    @ViewBuilder
437
+    private func comparisonSection(baseline: HealthSnapshot) -> some View {
438
+        let delta = diffService.totalAbsoluteChange(current: currentSnapshot, baseline: baseline)
439
+        let deltaPercent = computeDeltaPercent(delta: delta, baseline: baseline)
440
+        let isSignificant = delta > 0 || (deltaPercent > 10)
441
+
442
+        DisclosureGroup {
443
+            VStack(alignment: .leading, spacing: 12) {
444
+                DetailRow(label: "Baseline") {
445
+                    Text(baseline.timestamp, format: .dateTime.month().day().hour().minute())
446
+                        .foregroundStyle(.secondary)
447
+                }
448
+                Divider()
449
+                DetailRow(label: "Time Span") {
450
+                    let days = Calendar.current.dateComponents([.day], from: baseline.timestamp, to: currentSnapshot.timestamp).day ?? 0
451
+                    Text(days == 0 ? "Same day" : "\(days) days")
452
+                        .foregroundStyle(.secondary)
453
+                }
362 454
             }
363
-            DetailRow(label: "OS") {
364
-                Text(currentSnapshot.osVersion)
365
-                    .foregroundStyle(.secondary)
455
+            .padding(.top, 8)
456
+        } label: {
457
+            HStack(spacing: 8) {
458
+                Image(systemName: "arrow.left.and.right.square")
459
+                    .font(.system(size: 16, weight: .semibold))
460
+                Text("Comparison")
461
+                    .font(.subheadline.weight(.semibold))
462
+                Spacer()
463
+                if isSignificant {
464
+                    SeverityBadge(delta: delta)
465
+                        .frame(height: 24)
466
+                } else {
467
+                    Text("–")
468
+                        .font(.caption2.weight(.semibold))
469
+                        .foregroundStyle(.secondary)
470
+                }
366 471
             }
367 472
         }
473
+        .padding(12)
474
+        .background(Color(.systemBackground).opacity(0.5), in: RoundedRectangle(cornerRadius: 12))
368 475
     }
369 476
 
370
-    private func comparisonSection(_ baseline: HealthSnapshot) -> some View {
371
-        Section("Comparison") {
372
-            DetailRow(label: "Baseline") {
373
-                Text(baseline.timestamp, format: .dateTime.year().month().day().hour().minute())
374
-                    .foregroundStyle(.secondary)
375
-            }
376
-            DetailRow(label: "Changes") {
377
-                let delta = diffService.totalAbsoluteChange(current: currentSnapshot, baseline: baseline)
378
-                Text(delta == 0 ? "None" : "\(delta) records")
379
-                    .foregroundStyle(delta == 0 ? Color.healthyGreen : Color.warningAmber)
380
-            }
477
+    private func shortOSVersion(_ full: String) -> Text {
478
+        if full.hasPrefix("iOS ") {
479
+            let version = full.dropFirst(4).prefix(while: { $0 != " " })
480
+            return Text("iOS \(version)")
481
+        }
482
+        return Text(full)
483
+    }
484
+
485
+    private func extractOSVersion(_ full: String) -> String {
486
+        if full.hasPrefix("iOS ") {
487
+            let versionPart = full.dropFirst(4).prefix(while: { $0 != " " && $0 != "(" })
488
+            return String(versionPart)
489
+        }
490
+        return full
491
+    }
492
+    
493
+    private func extractBuildNumber(_ full: String) -> String {
494
+        if let start = full.firstIndex(of: "("), let end = full.firstIndex(of: ")") {
495
+            let buildPart = String(full[full.index(after: start)..<end])
496
+            return buildPart.hasPrefix("Build ") ? String(buildPart.dropFirst(6)) : buildPart
381 497
         }
498
+        return full
499
+    }
500
+
501
+    private func computeDeltaPercent(delta: Int, baseline: HealthSnapshot) -> Double {
502
+        let baselineTotal = (baseline.typeCounts ?? [])
503
+            .filter { $0.count > 0 }
504
+            .reduce(0) { $0 + $1.count }
505
+        return baselineTotal > 0 ? Double(delta) / Double(baselineTotal) * 100 : 0
506
+    }
507
+
508
+    private func statCompact(label: String, value: String) -> some View {
509
+        VStack(alignment: .center, spacing: 2) {
510
+            Text(label)
511
+                .font(.caption2.weight(.medium))
512
+            Text(value)
513
+                .font(.subheadline.weight(.semibold).monospacedDigit())
514
+                .foregroundStyle(.primary)
515
+        }
516
+        .frame(maxWidth: .infinity)
382 517
     }
383 518
 
384 519
     private var evolutionSection: some View {
385 520
         Section("Data Types") {
386 521
             HStack {
387
-                Text("X-Axis")
388
-                    .foregroundStyle(.secondary)
389 522
                 Spacer()
390 523
                 Picker("X-Axis", selection: $xAxisMode) {
391
-                    ForEach(EvolutionXAxisMode.allCases) { mode in
524
+                    ForEach(EvolutionXAxisMode.allCases.reversed()) { mode in
392 525
                         Text(mode.title).tag(mode)
393 526
                     }
394 527
                 }
395 528
                 .pickerStyle(.segmented)
396
-                .frame(maxWidth: 220)
529
+                Spacer()
397 530
             }
398 531
 
399 532
             if evolutionSeries.isEmpty {
@@ -541,6 +674,12 @@ private struct TypeEvolutionChart: View {
541 674
         return selected.count - previous.count
542 675
     }
543 676
 
677
+    private var isSignificantChange: Bool {
678
+        guard let d = delta, let prev = previousPoint?.count, prev > 0 else { return false }
679
+        let percentChange = abs(Double(d)) / Double(prev) * 100
680
+        return percentChange > 10 || d > 0
681
+    }
682
+
544 683
     private var contextPointCountLabel: String {
545 684
         "\(series.points.count)/\(contextSnapshots.count) snapshots with data"
546 685
     }
@@ -710,7 +849,7 @@ private struct TypeEvolutionChart: View {
710 849
                             .font(.subheadline.monospacedDigit())
711 850
                             .foregroundStyle(.secondary)
712 851
                     }
713
-                    if let delta {
852
+                    if isSignificantChange, let delta {
714 853
                         SeverityBadge(delta: delta)
715 854
                     }
716 855
                 }
@@ -781,6 +920,12 @@ private struct SnapshotTypeCountRow: View {
781 920
         return typeCount.count - b.count
782 921
     }
783 922
 
923
+    private var isSignificantChange: Bool {
924
+        guard let d = delta, let b = baselineTypeCount?.count, b > 0 else { return false }
925
+        let percentChange = abs(Double(d)) / Double(b) * 100
926
+        return percentChange > 10 || d > 0
927
+    }
928
+
784 929
     var body: some View {
785 930
         HStack(spacing: 12) {
786 931
             VStack(alignment: .leading, spacing: 3) {
@@ -797,7 +942,7 @@ private struct SnapshotTypeCountRow: View {
797 942
                 Text(countText)
798 943
                     .font(.subheadline.monospacedDigit())
799 944
                     .foregroundStyle(countColor)
800
-                if let delta {
945
+                if isSignificantChange, let delta {
801 946
                     SeverityBadge(delta: delta)
802 947
                 }
803 948
             }
@@ -806,6 +951,130 @@ private struct SnapshotTypeCountRow: View {
806 951
     }
807 952
 }
808 953
 
954
+private struct SnapshotDataRangeIndicator: View {
955
+    let oldestRecordDate: Date?
956
+    let newestRecordDate: Date?
957
+    let quality: SnapshotQuality
958
+
959
+    private var hasDateRange: Bool {
960
+        oldestRecordDate != nil && newestRecordDate != nil
961
+    }
962
+
963
+    private var daySpan: Int? {
964
+        guard let oldest = oldestRecordDate, let newest = newestRecordDate else { return nil }
965
+        return Calendar.current.dateComponents([.day], from: oldest, to: newest).day ?? 0
966
+    }
967
+
968
+    var body: some View {
969
+        VStack(spacing: 12) {
970
+            HStack(spacing: 8) {
971
+                Text("Data Range")
972
+                    .font(.headline.weight(.semibold))
973
+
974
+                Spacer()
975
+
976
+                qualityBadge
977
+            }
978
+
979
+            if hasDateRange {
980
+                dateRangeVisualization
981
+            } else {
982
+                Text("No dated records available")
983
+                    .font(.subheadline)
984
+                    .foregroundStyle(.secondary)
985
+                    .frame(maxWidth: .infinity, alignment: .center)
986
+                    .padding(.vertical, 16)
987
+            }
988
+        }
989
+        .padding(16)
990
+        .background(Color(.systemBackground).opacity(0.5), in: RoundedRectangle(cornerRadius: 12))
991
+    }
992
+
993
+    @ViewBuilder
994
+    private var qualityBadge: some View {
995
+        if quality != .complete {
996
+            Label("Incomplete", systemImage: "exclamationmark.triangle.fill")
997
+                .font(.caption.weight(.medium))
998
+                .foregroundStyle(Color.warningAmber)
999
+                .padding(.horizontal, 8)
1000
+                .padding(.vertical, 4)
1001
+                .background(Color.warningAmber.opacity(0.12), in: Capsule())
1002
+        }
1003
+    }
1004
+
1005
+    @ViewBuilder
1006
+    private var dateRangeVisualization: some View {
1007
+        if let oldest = oldestRecordDate, let newest = newestRecordDate, let span = daySpan {
1008
+            VStack(spacing: 12) {
1009
+                HStack(alignment: .top, spacing: 12) {
1010
+                    VStack(alignment: .center, spacing: 4) {
1011
+                        Image(systemName: "calendar.badge.clock")
1012
+                            .font(.system(size: 16, weight: .semibold))
1013
+                            .foregroundStyle(Color.healthyGreen)
1014
+
1015
+                        VStack(alignment: .center, spacing: 2) {
1016
+                            Text("Oldest record")
1017
+                                .font(.caption2.weight(.medium))
1018
+                                .foregroundStyle(.secondary)
1019
+                            Text(oldest, format: .dateTime.month().day().year())
1020
+                                .font(.caption.weight(.semibold))
1021
+                        }
1022
+                    }
1023
+                    .frame(maxWidth: .infinity)
1024
+
1025
+                    VStack(alignment: .center, spacing: 4) {
1026
+                        Text("\(span)")
1027
+                            .font(.system(size: 18, weight: .semibold).monospacedDigit())
1028
+                            .foregroundStyle(.primary)
1029
+
1030
+                        Text("days")
1031
+                            .font(.caption2.weight(.medium))
1032
+                            .foregroundStyle(.secondary)
1033
+                    }
1034
+
1035
+                    VStack(alignment: .center, spacing: 4) {
1036
+                        Image(systemName: "calendar.badge.clock")
1037
+                            .font(.system(size: 16, weight: .semibold))
1038
+                            .foregroundStyle(Color.accentColor)
1039
+
1040
+                        VStack(alignment: .center, spacing: 2) {
1041
+                            Text("Newest record")
1042
+                                .font(.caption2.weight(.medium))
1043
+                                .foregroundStyle(.secondary)
1044
+                            Text(newest, format: .dateTime.month().day().year())
1045
+                                .font(.caption.weight(.semibold))
1046
+                        }
1047
+                    }
1048
+                    .frame(maxWidth: .infinity)
1049
+                }
1050
+
1051
+                timelineBar
1052
+            }
1053
+        }
1054
+    }
1055
+
1056
+    @ViewBuilder
1057
+    private var timelineBar: some View {
1058
+        if oldestRecordDate != nil, newestRecordDate != nil {
1059
+            ZStack(alignment: .leading) {
1060
+                RoundedRectangle(cornerRadius: 3)
1061
+                    .fill(Color(.systemGray5))
1062
+
1063
+                RoundedRectangle(cornerRadius: 3)
1064
+                    .fill(
1065
+                        LinearGradient(
1066
+                            gradient: Gradient(colors: [Color.healthyGreen, Color.accentColor]),
1067
+                            startPoint: .leading,
1068
+                            endPoint: .trailing
1069
+                        )
1070
+                    )
1071
+                    .opacity(0.7)
1072
+            }
1073
+            .frame(height: 4)
1074
+        }
1075
+    }
1076
+}
1077
+
809 1078
 private struct DetailRow<Content: View>: View {
810 1079
     let label: String
811 1080
     @ViewBuilder let content: () -> Content