Showing 8 changed files with 234 additions and 25 deletions
+5 -5
HealthProbe/Doc/04-project/IMPLEMENTATION_STATUS.md
@@ -28,10 +28,10 @@ There are no real deployments, only test installations. Existing prototype datab
28 28
 | SQLite archive | Archive v2 schema, snapshot-level observation grouping, differential write path, v2 verification/delete bookkeeping, daily aggregate rebuilds, integrity report, v2 record reads, SQL diff/count/aggregate/provenance/consolidation-evidence APIs, large synthetic diff pagination coverage, formal timing/memory metrics, and XCTest coverage are in place; the legacy `archive_samples` mirror has been removed | Move Snapshots/Data Types from SwiftData previews to archive/cache DTOs |
29 29
 | Core Data cache | Initial programmatic Core Data model, full-cache rebuild service, read DTOs for observation/type/diff/health rows, and Dashboard archive-cache status wiring are in place | Move remaining export/report paths to cache DTOs and add targeted partial invalidation |
30 30
 | SwiftData cache | Exists; test builds now reset legacy prototype UI/archive/cache stores once for archive v2 so old SwiftData-only snapshots are not treated as backed-up observations | Treat as disposable prototype data; reset/ignore during v2 transition |
31
-| UI | Prototype exists; Snapshots/Data Types now default to the local device timeline instead of a multi-device picker. Dashboard status prefers archive/cache observation rows and shows cache health; Snapshots timeline, snapshot detail summaries/type rows, and Data Types list prefer Core Data cache rows when archive observation ids exist; data type detail reads Core Data type/diff summaries and uses SQLite `diffRecords` for paged drill-down; export preview reads the archive export API before showing/exporting JSON; visible change labels now use neutral new/missing/change-review language, with SwiftData detail cache as transition fallback | Add legacy/small-device simplifications and remove remaining SwiftData navigation handles |
31
+| UI | Prototype exists; Snapshots/Data Types now default to the local device timeline instead of a multi-device picker. Dashboard status prefers archive/cache observation rows and shows cache health; Snapshots timeline, snapshot detail summaries/type rows, and Data Types list prefer Core Data cache rows when archive observation ids exist; data type detail reads Core Data type/diff summaries and uses SQLite `diffRecords` for paged drill-down; export preview reads the archive export API before showing/exporting JSON; simplified detail mode replaces heavy charts with summary rows on small/accessibility layouts or when enabled in Settings; visible change labels now use neutral new/missing/change-review language, with SwiftData detail cache as transition fallback | Remove remaining SwiftData navigation handles |
32 32
 | Diff/change explanation | Prototype/legacy anomaly logic exists | Move heavy diffing into SQLite and use neutral change classifications |
33 33
 | Export | SQLite export preview, paged JSON writing, SHA256 manifest hashing, and `export_manifests` rows are in place for selected records and observation diffs | Fill remaining recovery-compatible envelope metadata, CSV export, relationship preservation, and reproducibility checks |
34
-| Legacy device support | Not implemented | Remove SwiftData dependency and simplify heavy views for low-memory devices |
34
+| Legacy device support | Simplified detail UI mode is implemented for small/accessibility layouts and as a Settings toggle | Remove SwiftData dependency and validate lower deployment targets |
35 35
 | Recovery workflows | Not supported | Preserve export/archive structure for external recovery tools only |
36 36
 
37 37
 ## Refactoring Priorities
@@ -42,7 +42,7 @@ Detailed checkable milestones live in [`Refactoring-Plan.md`](Refactoring-Plan.m
42 42
 2. Add targeted cache invalidation for affected observation/type ranges.
43 43
 3. Finish remaining UI language cleanup from anomaly/status to observation/diff/export where legacy model names still leak into active flows.
44 44
 4. Complete recovery-compatible export metadata, CSV output, and reproducibility checks.
45
-5. Validate on low-memory/legacy-class devices.
45
+5. Remove SwiftData dependency and validate lower deployment targets.
46 46
 
47 47
 ## Known Prototype Mismatches
48 48
 
@@ -52,7 +52,7 @@ Detailed checkable milestones live in [`Refactoring-Plan.md`](Refactoring-Plan.m
52 52
 - Current UI/cache layers still depend on SwiftData prototype models for capture review actions, navigation handles, some charts, and PDF paths.
53 53
 - Snapshots timeline, snapshot detail summary/type rows, Data Types list rows, and record drill-down are archive/cache-backed for new archive v2 observations when cache rows exist, but navigation still uses SwiftData snapshot handles during the transition.
54 54
 - Legacy SwiftData-only snapshots can show diffs without archive-backed values; they are now reset for archive v2 test installs rather than migrated.
55
-- Existing implementation may decode or cache too much data for low-end devices.
55
+- Capture strategy and some legacy SwiftData transition paths may still decode or cache too much data for low-end devices.
56 56
 - Old prototype database compatibility is no longer required.
57 57
 - Initial SQLite archive tests cover open/init/reset/idempotency, snapshot-level observation grouping, legacy mirror removal, small observation diffs, large synthetic diff pagination, formal timing/memory metrics, materialized aggregate comparison, source/provenance breakdowns, consolidation-evidence labels, export preview, paged JSON output, and manifest row persistence.
58 58
 - Initial Core Data cache tests cover full rebuild from SQLite and delete-cache-then-rebuild without losing archive data.
@@ -74,7 +74,7 @@ Detailed checkable milestones live in [`Refactoring-Plan.md`](Refactoring-Plan.m
74 74
 - [x] Export manifests include hashes and diff observation metadata when applicable.
75 75
 - [ ] iOS app remains read-only with respect to HealthKit.
76 76
 - [ ] Docs and UI do not claim in-app restore/re-publication support.
77
-- [ ] Legacy/small-device UI mode preserves capture/report/export.
77
+- [x] Legacy/small-device UI mode preserves capture/report/export.
78 78
 
79 79
 ## Historical Notes
80 80
 
+2 -2
HealthProbe/Doc/04-project/Refactoring-Plan.md
@@ -231,14 +231,14 @@ Checklist:
231 231
 - [x] Data type screens use target change labels.
232 232
 - [x] Export preview uses export query/manifest APIs.
233 233
 - [x] Archive status reflects SQLite/Core Data cache health.
234
-- [ ] Legacy/small-device UI mode simplifies heavy visualizations.
234
+- [x] Legacy/small-device UI mode simplifies heavy visualizations.
235 235
 
236 236
 Acceptance:
237 237
 - [ ] Core Time Machine flows work without SwiftData as target persistence.
238 238
 - [ ] UI copy uses observation/diff/export language.
239 239
 - [ ] No count-only critical data loss messaging.
240 240
 - [ ] Large record tables are paged.
241
-- [ ] Legacy mode preserves capture/report/export.
241
+- [x] Legacy mode preserves capture/report/export.
242 242
 
243 243
 ## Milestone 9 - Legacy SwiftData Retirement
244 244
 
+9 -0
HealthProbe/Utilities/AppSettings.swift
@@ -4,6 +4,7 @@ import Foundation
4 4
 final class AppSettings {
5 5
     private static let selectedTypeIDsKey   = "hp_selectedTypeIDs"
6 6
     static let adaptiveTimeoutsEnabledKey = "hp_adaptiveTimeoutsEnabled"
7
+    static let simplifiedUIModeEnabledKey = "hp_simplifiedUIModeEnabled"
7 8
     static let typeDetailCacheBackfillVersionKey = "hp_typeDetailCacheBackfillVersion"
8 9
     static let currentTypeDetailCacheBackfillVersion = 2
9 10
 
@@ -17,6 +18,12 @@ final class AppSettings {
17 18
         }
18 19
     }
19 20
 
21
+    var simplifiedUIModeEnabled: Bool {
22
+        didSet {
23
+            UserDefaults.standard.set(simplifiedUIModeEnabled, forKey: Self.simplifiedUIModeEnabledKey)
24
+        }
25
+    }
26
+
20 27
     static var currentDeviceID: String {
21 28
         KeychainService.resolveDeviceID(swiftDataStoreIsEmpty: false).id
22 29
     }
@@ -34,6 +41,8 @@ final class AppSettings {
34 41
         } else {
35 42
             adaptiveTimeoutsEnabled = UserDefaults.standard.bool(forKey: Self.adaptiveTimeoutsEnabledKey)
36 43
         }
44
+
45
+        simplifiedUIModeEnabled = UserDefaults.standard.bool(forKey: Self.simplifiedUIModeEnabledKey)
37 46
     }
38 47
 
39 48
     func isEnabled(_ type: MonitoredType) -> Bool { selectedTypeIDs.contains(type.id) }
+14 -0
HealthProbe/Utilities/LegacyUIMode.swift
@@ -0,0 +1,14 @@
1
+import SwiftUI
2
+
3
+enum LegacyUIMode {
4
+    static func isEnabled(
5
+        forceEnabled: Bool,
6
+        horizontalSizeClass: UserInterfaceSizeClass?,
7
+        dynamicTypeSize: DynamicTypeSize,
8
+        screenWidth: CGFloat
9
+    ) -> Bool {
10
+        forceEnabled ||
11
+        dynamicTypeSize.isAccessibilitySize ||
12
+        (horizontalSizeClass == .compact && screenWidth <= 390)
13
+    }
14
+}
+84 -16
HealthProbe/Views/DataTypes/DataTypeTemporalDistributionView.swift
@@ -6,13 +6,16 @@ struct DataTypeTemporalDistributionView: View {
6 6
     let previous: TypeCount?
7 7
     let displayName: String
8 8
 
9
+    @Environment(AppSettings.self) private var appSettings
9 10
     @Environment(\.modelContext) private var modelContext
11
+    @Environment(\.dynamicTypeSize) private var dynamicTypeSize
12
+    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
10 13
     @State private var viewModel = DataTypeTemporalDistributionViewModel()
11 14
     @State private var isRecomputing = false
12 15
     @State private var isZoomed: Bool = false
13 16
     @State private var displayedSnapshot: HealthSnapshot?
17
+    @State private var contentWidth: CGFloat = 744
14 18
     @Query(sort: \HealthSnapshot.timestamp) private var allSnapshots: [HealthSnapshot]
15
-    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
16 19
     @Environment(\.verticalSizeClass) private var verticalSizeClass
17 20
 
18 21
     private var isLandscape: Bool {
@@ -44,6 +47,15 @@ struct DataTypeTemporalDistributionView: View {
44 47
         currentSnapshot?.nextInTimeline(timelineSnapshots)
45 48
     }
46 49
 
50
+    private var usesSimplifiedDetailUI: Bool {
51
+        LegacyUIMode.isEnabled(
52
+            forceEnabled: appSettings.simplifiedUIModeEnabled,
53
+            horizontalSizeClass: horizontalSizeClass,
54
+            dynamicTypeSize: dynamicTypeSize,
55
+            screenWidth: contentWidth
56
+        )
57
+    }
58
+
47 59
     var body: some View {
48 60
         Group {
49 61
             if viewModel.isLoading {
@@ -58,25 +70,28 @@ struct DataTypeTemporalDistributionView: View {
58 70
         }
59 71
         .navigationTitle(displayName)
60 72
         .navigationBarTitleDisplayMode(.inline)
73
+        .background(contentWidthReader)
61 74
         .toolbar {
62 75
             ToolbarItemGroup(placement: .topBarTrailing) {
63
-                if canResetZoom {
76
+                if canResetZoom && !usesSimplifiedDetailUI {
64 77
                     Button(action: resetZoom) {
65 78
                         Image(systemName: "arrow.up.left.and.arrow.down.right")
66 79
                     }
67 80
                     .accessibilityLabel("Reset zoom")
68 81
                 }
69 82
 
70
-                Menu {
71
-                    Picker("Rezoluție", selection: $viewModel.binningStrategy) {
72
-                        ForEach(BinningStrategy.allCases, id: \.self) { strategy in
73
-                            Text(strategy.rawValue).tag(strategy)
83
+                if !usesSimplifiedDetailUI {
84
+                    Menu {
85
+                        Picker("Resolution", selection: $viewModel.binningStrategy) {
86
+                            ForEach(BinningStrategy.allCases, id: \.self) { strategy in
87
+                                Text(strategy.rawValue).tag(strategy)
88
+                            }
74 89
                         }
90
+                    } label: {
91
+                        Image(systemName: "line.3.horizontal.decrease.circle")
75 92
                     }
76
-                } label: {
77
-                    Image(systemName: "line.3.horizontal.decrease.circle")
93
+                    .accessibilityLabel("Select resolution")
78 94
                 }
79
-                .accessibilityLabel("Select resolution")
80 95
             }
81 96
         }
82 97
         .task {
@@ -84,6 +99,18 @@ struct DataTypeTemporalDistributionView: View {
84 99
         }
85 100
     }
86 101
 
102
+    private var contentWidthReader: some View {
103
+        GeometryReader { proxy in
104
+            Color.clear
105
+                .onAppear {
106
+                    contentWidth = proxy.size.width
107
+                }
108
+                .onChange(of: proxy.size.width) { _, newWidth in
109
+                    contentWidth = newWidth
110
+                }
111
+        }
112
+    }
113
+
87 114
     private var loadingView: some View {
88 115
         VStack(spacing: 12) {
89 116
             ProgressView()
@@ -136,13 +163,17 @@ struct DataTypeTemporalDistributionView: View {
136 163
             VStack(spacing: 16) {
137 164
                 summarySection
138 165
 
139
-                TemporalDistributionChartView(
140
-                    bins: viewModel.allBins,
141
-                    displayName: displayName,
142
-                    binningStrategy: viewModel.binningStrategy,
143
-                    onBinTap: handleBinTap,
144
-                    onResetZoom: resetZoom
145
-                )
166
+                if usesSimplifiedDetailUI {
167
+                    simplifiedDistributionRows
168
+                } else {
169
+                    TemporalDistributionChartView(
170
+                        bins: viewModel.allBins,
171
+                        displayName: displayName,
172
+                        binningStrategy: viewModel.binningStrategy,
173
+                        onBinTap: handleBinTap,
174
+                        onResetZoom: resetZoom
175
+                    )
176
+                }
146 177
 
147 178
                 statisticsSection
148 179
             }
@@ -160,6 +191,42 @@ struct DataTypeTemporalDistributionView: View {
160 191
         }
161 192
     }
162 193
 
194
+    private var simplifiedDistributionRows: some View {
195
+        VStack(alignment: .leading, spacing: 12) {
196
+            Text("Buckets")
197
+                .font(.headline.weight(.semibold))
198
+
199
+            ForEach(Array(viewModel.allBins.prefix(24).enumerated()), id: \.offset) { _, bin in
200
+                VStack(alignment: .leading, spacing: 6) {
201
+                    Text(bin.date.formatted(.dateTime.year().month().day()))
202
+                        .font(.caption.weight(.semibold))
203
+                        .foregroundStyle(.secondary)
204
+
205
+                    HStack(spacing: 12) {
206
+                        compactBucketValue("New", bin.added, .healthyGreen)
207
+                        compactBucketValue("Missing", bin.disappeared, .criticalRed)
208
+                        compactBucketValue("Same", bin.unchanged, .secondary)
209
+                    }
210
+                }
211
+                .padding(10)
212
+                .background(Color(.systemBackground).opacity(0.5), in: RoundedRectangle(cornerRadius: 8))
213
+            }
214
+        }
215
+        .frame(maxWidth: .infinity, alignment: .leading)
216
+    }
217
+
218
+    private func compactBucketValue(_ label: String, _ value: Int, _ color: Color) -> some View {
219
+        VStack(alignment: .leading, spacing: 2) {
220
+            Text(label)
221
+                .font(.caption2)
222
+                .foregroundStyle(.secondary)
223
+            Text("\(value)")
224
+                .font(.caption.weight(.semibold).monospacedDigit())
225
+                .foregroundStyle(color)
226
+        }
227
+        .frame(maxWidth: .infinity, alignment: .leading)
228
+    }
229
+
163 230
     private var summarySection: some View {
164 231
         VStack(alignment: .leading, spacing: 12) {
165 232
             HStack {
@@ -327,6 +394,7 @@ struct DataTypeTemporalDistributionView: View {
327 394
         )
328 395
     }
329 396
     .modelContainer(for: [HealthSnapshot.self, TypeCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true)
397
+    .environment(AppSettings())
330 398
 }
331 399
 
332 400
 extension TypeCount {
+10 -0
HealthProbe/Views/Settings/SettingsView.swift
@@ -26,6 +26,7 @@ struct SettingsView: View {
26 26
         NavigationStack {
27 27
             List {
28 28
                 deviceSection
29
+                interfaceSection
29 30
                 monitoringSection
30 31
                 timeoutCalibrationSection
31 32
                 typeSelectionSections
@@ -121,6 +122,15 @@ struct SettingsView: View {
121 122
         }
122 123
     }
123 124
 
125
+    private var interfaceSection: some View {
126
+        Section("Interface") {
127
+            Toggle("Simplified Detail Views", isOn: Binding(
128
+                get: { appSettings.simplifiedUIModeEnabled },
129
+                set: { appSettings.simplifiedUIModeEnabled = $0 }
130
+            ))
131
+        }
132
+    }
133
+
124 134
     private var timeoutCalibrationSection: some View {
125 135
         Section("Timeout Calibration") {
126 136
             Toggle("Adaptive Timeouts", isOn: Binding(
+69 -2
HealthProbe/Views/Snapshots/DataTypeSnapshotDetailView.swift
@@ -6,7 +6,10 @@ struct DataTypeSnapshotDetailView: View {
6 6
     let typeIdentifier: String
7 7
     let displayName: String
8 8
 
9
+    @Environment(AppSettings.self) private var appSettings
9 10
     @Environment(\.modelContext) private var modelContext
11
+    @Environment(\.dynamicTypeSize) private var dynamicTypeSize
12
+    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
10 13
     @Query(sort: \HealthSnapshot.timestamp) private var allSnapshots: [HealthSnapshot]
11 14
     @Query private var allDeltas: [SnapshotDelta]
12 15
 
@@ -18,6 +21,7 @@ struct DataTypeSnapshotDetailView: View {
18 21
     @State private var detailCacheDiagnostic: String?
19 22
     @State private var currentCachedTypeSummary: CachedArchiveTypeSummary?
20 23
     @State private var previousCachedTypeSummary: CachedArchiveTypeSummary?
24
+    @State private var contentWidth: CGFloat = 744
21 25
 
22 26
     private var currentSnapshot: HealthSnapshot {
23 27
         displayedSnapshot ?? snapshot
@@ -111,6 +115,13 @@ struct DataTypeSnapshotDetailView: View {
111 115
         return (max(net, 0), max(-net, 0), false)
112 116
     }
113 117
 
118
+    private var simplifiedRecordCounts: (added: Int, disappeared: Int, exact: Bool) {
119
+        if case .loaded(let diff) = diffState {
120
+            return (diff.addedCount, diff.disappearedCount, true)
121
+        }
122
+        return quickAddedDisappeared
123
+    }
124
+
114 125
     private var currentArchiveObservationID: Int64? {
115 126
         currentSnapshot.archiveObservationID
116 127
     }
@@ -127,6 +138,15 @@ struct DataTypeSnapshotDetailView: View {
127 138
         currentArchiveObservationID != nil
128 139
     }
129 140
 
141
+    private var usesSimplifiedDetailUI: Bool {
142
+        LegacyUIMode.isEnabled(
143
+            forceEnabled: appSettings.simplifiedUIModeEnabled,
144
+            horizontalSizeClass: horizontalSizeClass,
145
+            dynamicTypeSize: dynamicTypeSize,
146
+            screenWidth: contentWidth
147
+        )
148
+    }
149
+
130 150
     var body: some View {
131 151
         ScrollView {
132 152
             VStack(spacing: 16) {
@@ -137,8 +157,12 @@ struct DataTypeSnapshotDetailView: View {
137 157
                 } else {
138 158
                     dataRangeSection
139 159
                     recordChangeComparisonSection
140
-                    recordChangeEvolutionSection
141
-                    temporalDistributionSection
160
+                    if usesSimplifiedDetailUI {
161
+                        simplifiedDetailSection
162
+                    } else {
163
+                        recordChangeEvolutionSection
164
+                        temporalDistributionSection
165
+                    }
142 166
                 }
143 167
             }
144 168
             .padding(16)
@@ -158,6 +182,7 @@ struct DataTypeSnapshotDetailView: View {
158 182
                 snapshotToolbarTitle
159 183
             }
160 184
         }
185
+        .background(contentWidthReader)
161 186
         .task(id: diffTaskID) {
162 187
             await loadArchiveTypeSummaries()
163 188
             await loadRecordDiff()
@@ -171,6 +196,18 @@ struct DataTypeSnapshotDetailView: View {
171 196
         }
172 197
     }
173 198
 
199
+    private var contentWidthReader: some View {
200
+        GeometryReader { proxy in
201
+            Color.clear
202
+                .onAppear {
203
+                    contentWidth = proxy.size.width
204
+                }
205
+                .onChange(of: proxy.size.width) { _, newWidth in
206
+                    contentWidth = newWidth
207
+                }
208
+        }
209
+    }
210
+
174 211
     private func typeCount(in snapshot: HealthSnapshot) -> TypeCount? {
175 212
         snapshot.typeCounts?.first { $0.typeIdentifier == typeIdentifier }
176 213
     }
@@ -386,6 +423,35 @@ struct DataTypeSnapshotDetailView: View {
386 423
         }
387 424
     }
388 425
 
426
+    private var simplifiedDetailSection: some View {
427
+        VStack(alignment: .leading, spacing: 12) {
428
+            Text("Summary")
429
+                .font(.headline.weight(.semibold))
430
+
431
+            HStack(spacing: 12) {
432
+                quickStat(label: "Current", value: "\(quickCurrentCountValue)")
433
+                quickStat(label: "Previous", value: "\(quickPreviousCountValue)")
434
+            }
435
+
436
+            let quick = simplifiedRecordCounts
437
+            HStack(spacing: 12) {
438
+                quickStat(label: "New", value: "\(quick.added)", color: .healthyGreen)
439
+                quickStat(label: "Missing", value: "\(quick.disappeared)", color: .criticalRed)
440
+            }
441
+
442
+            if !quick.exact {
443
+                Text("Exact split needs record analysis.")
444
+                    .font(.caption2)
445
+                    .foregroundStyle(.secondary)
446
+            }
447
+        }
448
+        .padding(12)
449
+        .frame(maxWidth: .infinity, alignment: .leading)
450
+        .background(Color(.systemBackground).opacity(0.5), in: RoundedRectangle(cornerRadius: 8))
451
+        .accessibilityElement(children: .combine)
452
+        .accessibilityLabel("Summary. Current \(quickCurrentCountValue). Previous \(quickPreviousCountValue). New \(simplifiedRecordCounts.added). Missing \(simplifiedRecordCounts.disappeared).")
453
+    }
454
+
389 455
     @ViewBuilder
390 456
     private var temporalDistributionSection: some View {
391 457
         if previousSnapshot != nil, currentTypeCount != nil, !isCurrentTypeContentAliasToPrevious {
@@ -710,4 +776,5 @@ private struct DataTypeRecordDiff: Equatable, Sendable {
710 776
         )
711 777
     }
712 778
     .modelContainer(for: [HealthSnapshot.self, TypeCount.self, YearlyCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true)
779
+    .environment(AppSettings())
713 780
 }
+41 -0
HealthProbeTests/LegacyUIModeTests.swift
@@ -0,0 +1,41 @@
1
+import SwiftUI
2
+import XCTest
3
+@testable import HealthProbe
4
+
5
+final class LegacyUIModeTests: XCTestCase {
6
+    func testForcedSimplifiedModeWins() {
7
+        XCTAssertTrue(LegacyUIMode.isEnabled(
8
+            forceEnabled: true,
9
+            horizontalSizeClass: .regular,
10
+            dynamicTypeSize: .large,
11
+            screenWidth: 744
12
+        ))
13
+    }
14
+
15
+    func testCompactSmallScreenUsesSimplifiedMode() {
16
+        XCTAssertTrue(LegacyUIMode.isEnabled(
17
+            forceEnabled: false,
18
+            horizontalSizeClass: .compact,
19
+            dynamicTypeSize: .large,
20
+            screenWidth: 375
21
+        ))
22
+    }
23
+
24
+    func testAccessibilityDynamicTypeUsesSimplifiedMode() {
25
+        XCTAssertTrue(LegacyUIMode.isEnabled(
26
+            forceEnabled: false,
27
+            horizontalSizeClass: .regular,
28
+            dynamicTypeSize: .accessibility1,
29
+            screenWidth: 744
30
+        ))
31
+    }
32
+
33
+    func testRegularComfortableLayoutKeepsFullMode() {
34
+        XCTAssertFalse(LegacyUIMode.isEnabled(
35
+            forceEnabled: false,
36
+            horizontalSizeClass: .regular,
37
+            dynamicTypeSize: .large,
38
+            screenWidth: 744
39
+        ))
40
+    }
41
+}