Showing 3 changed files with 414 additions and 141 deletions
+101 -26
USB Meter/Views/Meter/ControlView.swift
@@ -11,6 +11,8 @@ import SwiftUI
11 11
 struct ControlView: View {
12 12
     
13 13
     @EnvironmentObject private var meter: Meter
14
+    var compactLayout: Bool = false
15
+    var availableSize: CGSize? = nil
14 16
 
15 17
     var body: some View {
16 18
         VStack(alignment: .leading, spacing: 14) {
@@ -23,52 +25,125 @@ struct ControlView: View {
23 25
                     .foregroundColor(.secondary)
24 26
             }
25 27
 
26
-            HStack(spacing: 12) {
27
-                controlButton(
28
-                    title: "Prev",
29
-                    symbol: "chevron.left",
30
-                    tint: .indigo,
31
-                    action: { meter.previousScreen() }
32
-                )
28
+            if compactLayout {
29
+                if usesExpandedCompactLayout {
30
+                    Spacer(minLength: 0)
31
+
32
+                    VStack(spacing: 12) {
33
+                        HStack(spacing: 12) {
34
+                            controlButton(
35
+                                title: "Prev",
36
+                                symbol: "chevron.left",
37
+                                tint: .indigo,
38
+                                action: { meter.previousScreen() }
39
+                            )
40
+
41
+                            currentScreenCard
42
+                                .frame(maxWidth: .infinity, minHeight: 112)
43
+                                .padding(.horizontal, 14)
44
+                                .meterCard(tint: meter.color, fillOpacity: 0.06, strokeOpacity: 0.10)
45
+                        }
46
+
47
+                        HStack(spacing: 12) {
48
+                            controlButton(
49
+                                title: "Rotate",
50
+                                symbol: "rotate.right.fill",
51
+                                tint: .orange,
52
+                                action: { meter.rotateScreen() }
53
+                            )
54
+
55
+                            controlButton(
56
+                                title: "Next",
57
+                                symbol: "chevron.right",
58
+                                tint: .indigo,
59
+                                action: { meter.nextScreen() }
60
+                            )
61
+                        }
62
+                    }
63
+
64
+                    Spacer(minLength: 0)
65
+                } else {
66
+                    HStack(spacing: 10) {
67
+                        controlButton(
68
+                            title: "Prev",
69
+                            symbol: "chevron.left",
70
+                            tint: .indigo,
71
+                            action: { meter.previousScreen() }
72
+                        )
33 73
 
34
-                currentScreenCard
35
-                .frame(maxWidth: .infinity, minHeight: 92)
36
-                .padding(.horizontal, 12)
37
-                .meterCard(tint: meter.color, fillOpacity: 0.06, strokeOpacity: 0.10)
74
+                        currentScreenCard
75
+                            .frame(maxWidth: .infinity, minHeight: 82)
76
+                            .padding(.horizontal, 10)
77
+                            .meterCard(tint: meter.color, fillOpacity: 0.06, strokeOpacity: 0.10)
78
+
79
+                        controlButton(
80
+                            title: "Rotate",
81
+                            symbol: "rotate.right.fill",
82
+                            tint: .orange,
83
+                            action: { meter.rotateScreen() }
84
+                        )
85
+
86
+                        controlButton(
87
+                            title: "Next",
88
+                            symbol: "chevron.right",
89
+                            tint: .indigo,
90
+                            action: { meter.nextScreen() }
91
+                        )
92
+                    }
93
+                }
94
+            } else {
95
+                HStack(spacing: 12) {
96
+                    controlButton(
97
+                        title: "Prev",
98
+                        symbol: "chevron.left",
99
+                        tint: .indigo,
100
+                        action: { meter.previousScreen() }
101
+                    )
102
+
103
+                    currentScreenCard
104
+                    .frame(maxWidth: .infinity, minHeight: 92)
105
+                    .padding(.horizontal, 12)
106
+                    .meterCard(tint: meter.color, fillOpacity: 0.06, strokeOpacity: 0.10)
107
+
108
+                    controlButton(
109
+                        title: "Next",
110
+                        symbol: "chevron.right",
111
+                        tint: .indigo,
112
+                        action: { meter.nextScreen() }
113
+                    )
114
+                }
38 115
 
39 116
                 controlButton(
40
-                    title: "Next",
41
-                    symbol: "chevron.right",
42
-                    tint: .indigo,
43
-                    action: { meter.nextScreen() }
117
+                    title: "Rotate Screen",
118
+                    symbol: "rotate.right.fill",
119
+                    tint: .orange,
120
+                    compact: false,
121
+                    action: { meter.rotateScreen() }
44 122
                 )
45 123
             }
46
-
47
-            controlButton(
48
-                title: "Rotate Screen",
49
-                symbol: "rotate.right.fill",
50
-                tint: .orange,
51
-                compact: false,
52
-                action: { meter.rotateScreen() }
53
-            )
54 124
         }
125
+        .frame(maxWidth: .infinity, maxHeight: compactLayout ? .infinity : nil, alignment: .topLeading)
55 126
     }
56 127
 
57 128
     @ViewBuilder
58 129
     private var currentScreenCard: some View {
59 130
         if meter.reportsCurrentScreenIndex {
60 131
             Text(meter.currentScreenDescription)
61
-                .font(.subheadline.weight(.semibold))
132
+                .font((usesExpandedCompactLayout ? Font.title3 : .subheadline).weight(.semibold))
62 133
                 .multilineTextAlignment(.center)
63 134
         } else {
64 135
             VStack {
65 136
                 Image(systemName: "questionmark.square.dashed")
66
-                    .font(.system(size: 24, weight: .semibold))
137
+                    .font(.system(size: usesExpandedCompactLayout ? 30 : 24, weight: .semibold))
67 138
                     .foregroundColor(.secondary)
68 139
             }
69 140
         }
70 141
     }
71 142
 
143
+    private var usesExpandedCompactLayout: Bool {
144
+        compactLayout && (availableSize?.height ?? 0) >= 520
145
+    }
146
+
72 147
     private func controlButton(
73 148
         title: String,
74 149
         symbol: String,
@@ -85,7 +160,7 @@ struct ControlView: View {
85 160
                     .multilineTextAlignment(.center)
86 161
             }
87 162
             .foregroundColor(tint)
88
-            .frame(maxWidth: .infinity, minHeight: compact ? 92 : 68)
163
+            .frame(maxWidth: .infinity, minHeight: compact ? (usesExpandedCompactLayout ? 112 : 92) : 68)
89 164
             .padding(.horizontal, 8)
90 165
             .meterCard(tint: tint, fillOpacity: 0.10, strokeOpacity: 0.14)
91 166
         }
+103 -29
USB Meter/Views/Meter/LiveView.swift
@@ -11,6 +11,8 @@ import SwiftUI
11 11
 struct LiveView: View {
12 12
     
13 13
     @EnvironmentObject private var meter: Meter
14
+    var compactLayout: Bool = false
15
+    var availableSize: CGSize? = nil
14 16
 
15 17
     var body: some View {
16 18
         VStack(alignment: .leading, spacing: 16) {
@@ -21,7 +23,7 @@ struct LiveView: View {
21 23
                 statusBadge
22 24
             }
23 25
 
24
-            LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
26
+            LazyVGrid(columns: liveMetricColumns, spacing: compactLayout ? 10 : 12) {
25 27
                 liveMetricCard(
26 28
                     title: "Voltage",
27 29
                     symbol: "bolt.fill",
@@ -67,34 +69,72 @@ struct LiveView: View {
67 69
                 )
68 70
             }
69 71
 
72
+            if compactLayout && usesExpandedCompactLayout {
73
+                Spacer(minLength: 0)
74
+            }
75
+
70 76
             if shouldShowSecondaryDetails {
71
-                VStack(alignment: .leading, spacing: 12) {
72
-                    Text("Details")
73
-                        .font(.subheadline.weight(.semibold))
74
-                        .foregroundColor(.secondary)
75
-
76
-                    if meter.loadResistance > 0 {
77
-                        secondaryDetailRow(
78
-                            title: "Load",
79
-                            value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}",
80
-                            symbol: "cable.connector",
81
-                            color: .yellow
82
-                        )
83
-                    }
77
+                Group {
78
+                    if compactLayout {
79
+                        HStack(spacing: 12) {
80
+                            if meter.loadResistance > 0 {
81
+                                secondaryDetailChip(
82
+                                    title: "Load",
83
+                                    value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}",
84
+                                    symbol: "cable.connector",
85
+                                    color: .yellow
86
+                                )
87
+                            }
88
+
89
+                            if shouldShowChargerType {
90
+                                secondaryDetailChip(
91
+                                    title: "Charger",
92
+                                    value: meter.chargerTypeDescription,
93
+                                    symbol: "bolt.badge.checkmark",
94
+                                    color: .purple
95
+                                )
96
+                            }
97
+                        }
98
+                    } else {
99
+                        VStack(alignment: .leading, spacing: 12) {
100
+                            Text("Details")
101
+                                .font(.subheadline.weight(.semibold))
102
+                                .foregroundColor(.secondary)
84 103
 
85
-                    if shouldShowChargerType {
86
-                        secondaryDetailRow(
87
-                            title: "Charger",
88
-                            value: meter.chargerTypeDescription,
89
-                            symbol: "bolt.badge.checkmark",
90
-                            color: .purple
91
-                        )
104
+                            if meter.loadResistance > 0 {
105
+                                secondaryDetailRow(
106
+                                    title: "Load",
107
+                                    value: "\(meter.loadResistance.format(decimalDigits: 1)) \u{2126}",
108
+                                    symbol: "cable.connector",
109
+                                    color: .yellow
110
+                                )
111
+                            }
112
+
113
+                            if shouldShowChargerType {
114
+                                secondaryDetailRow(
115
+                                    title: "Charger",
116
+                                    value: meter.chargerTypeDescription,
117
+                                    symbol: "bolt.badge.checkmark",
118
+                                    color: .purple
119
+                                )
120
+                            }
121
+                        }
92 122
                     }
93 123
                 }
94
-                .padding(18)
124
+                .padding(compactLayout ? 14 : 18)
95 125
                 .meterCard(tint: meter.color, fillOpacity: 0.06, strokeOpacity: 0.10)
96 126
             }
97 127
         }
128
+        .frame(maxWidth: .infinity, maxHeight: compactLayout ? .infinity : nil, alignment: .topLeading)
129
+    }
130
+
131
+    private var liveMetricColumns: [GridItem] {
132
+        if compactLayout {
133
+            let count = usesExpandedCompactLayout ? 2 : 4
134
+            return Array(repeating: GridItem(.flexible(), spacing: 10), count: count)
135
+        }
136
+
137
+        return [GridItem(.flexible()), GridItem(.flexible())]
98 138
     }
99 139
 
100 140
     private var statusBadge: some View {
@@ -119,6 +159,10 @@ struct LiveView: View {
119 159
         meter.supportsChargerDetection && meter.chargerTypeDescription != "Unknown"
120 160
     }
121 161
 
162
+    private var usesExpandedCompactLayout: Bool {
163
+        compactLayout && (availableSize?.height ?? 0) >= 520
164
+    }
165
+
122 166
     private func liveMetricCard(
123 167
         title: String,
124 168
         symbol: String,
@@ -129,30 +173,34 @@ struct LiveView: View {
129 173
         VStack(alignment: .leading, spacing: 10) {
130 174
             HStack {
131 175
                 Image(systemName: symbol)
132
-                    .font(.system(size: 15, weight: .semibold))
176
+                    .font(.system(size: compactLayout ? 14 : 15, weight: .semibold))
133 177
                     .foregroundColor(color)
134
-                    .frame(width: 34, height: 34)
178
+                    .frame(width: compactLayout ? 30 : 34, height: compactLayout ? 30 : 34)
135 179
                     .background(Circle().fill(color.opacity(0.12)))
136 180
                 Spacer()
137 181
             }
138 182
 
139 183
             Text(title)
140
-                .font(.subheadline.weight(.semibold))
184
+                .font((compactLayout ? Font.caption : .subheadline).weight(.semibold))
141 185
                 .foregroundColor(.secondary)
142 186
 
143 187
             Text(value)
144
-                .font(.system(.title3, design: .rounded).weight(.bold))
188
+                .font(.system(compactLayout ? .headline : .title3, design: .rounded).weight(.bold))
145 189
                 .monospacedDigit()
146 190
 
147
-            if let range, !range.isEmpty {
191
+            if !compactLayout, let range, !range.isEmpty {
148 192
                 Text(range)
149 193
                     .font(.caption)
150 194
                     .foregroundColor(.secondary)
151 195
                     .lineLimit(2)
152 196
             }
153 197
         }
154
-        .frame(maxWidth: .infinity, minHeight: 128, alignment: .leading)
155
-        .padding(16)
198
+        .frame(
199
+            maxWidth: .infinity,
200
+            minHeight: compactLayout ? (usesExpandedCompactLayout ? 128 : 96) : 128,
201
+            alignment: .leading
202
+        )
203
+        .padding(compactLayout ? 12 : 16)
156 204
         .meterCard(tint: color, fillOpacity: 0.10, strokeOpacity: 0.12)
157 205
     }
158 206
 
@@ -176,6 +224,32 @@ struct LiveView: View {
176 224
         .font(.footnote)
177 225
     }
178 226
 
227
+    private func secondaryDetailChip(
228
+        title: String,
229
+        value: String,
230
+        symbol: String,
231
+        color: Color
232
+    ) -> some View {
233
+        HStack(spacing: 10) {
234
+            Image(systemName: symbol)
235
+                .foregroundColor(color)
236
+                .frame(width: 22, height: 22)
237
+                .background(Circle().fill(color.opacity(0.12)))
238
+
239
+            VStack(alignment: .leading, spacing: 2) {
240
+                Text(title)
241
+                    .foregroundColor(.secondary)
242
+                Text(value)
243
+                    .fontWeight(.semibold)
244
+                    .lineLimit(1)
245
+            }
246
+
247
+            Spacer(minLength: 0)
248
+        }
249
+        .font(.caption)
250
+        .frame(maxWidth: .infinity, alignment: .leading)
251
+    }
252
+
179 253
     private func rangeText(min: Double, max: Double, unit: String) -> String? {
180 254
         guard min.isFinite, max.isFinite else { return nil }
181 255
         return "Min \(min.format(decimalDigits: 3)) \(unit)  Max \(max.format(decimalDigits: 3)) \(unit)"
+210 -86
USB Meter/Views/Meter/MeterView.swift
@@ -25,12 +25,46 @@ struct MeterView: View {
25 25
     private let actionButtonHeight: CGFloat = 108
26 26
 
27 27
     var body: some View {
28
+        GeometryReader { proxy in
29
+            let landscape = isLandscape(size: proxy.size)
30
+
31
+            Group {
32
+                if landscape {
33
+                    landscapeDeck(size: proxy.size)
34
+                } else {
35
+                    portraitContent
36
+                }
37
+            }
38
+            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
39
+            .navigationBarHidden(landscape)
40
+        }
41
+        .background(meterBackground)
42
+        .navigationBarTitle("Meter")
43
+        .navigationBarItems(trailing: HStack (spacing: 6) {
44
+            if meter.operationalState > .notPresent {
45
+                RSSIView(RSSI: meter.btSerial.RSSI)
46
+                    .frame(width: 18, height: 18)
47
+                    .padding(.leading, 6)
48
+                    .padding(.vertical)
49
+            }
50
+            NavigationLink(destination: MeterInfoView().environmentObject(meter)) {
51
+                Image(systemName: "info.circle.fill")
52
+                    .padding(.vertical)
53
+            }
54
+            NavigationLink(destination: MeterSettingsView().environmentObject(meter)) {
55
+                Image(systemName: "gearshape.fill")
56
+                    .padding(.vertical)
57
+            }
58
+        })
59
+    }
60
+
61
+    private var portraitContent: some View {
28 62
         ScrollView {
29 63
             VStack(alignment: .leading, spacing: 16) {
30
-                connectionCard
64
+                connectionCard()
31 65
 
32 66
                 if meter.operationalState == .dataIsAvailable {
33
-                    actionGrid
67
+                    actionGrid()
34 68
 
35 69
                     LiveView()
36 70
                         .padding(18)
@@ -51,114 +85,196 @@ struct MeterView: View {
51 85
             }
52 86
             .padding()
53 87
         }
54
-        .background(
55
-            LinearGradient(
56
-                colors: [
57
-                    meter.color.opacity(0.22),
58
-                    Color.secondary.opacity(0.08),
59
-                    Color.clear
60
-                ],
61
-                startPoint: .topLeading,
62
-                endPoint: .bottomTrailing
63
-            )
64
-            .ignoresSafeArea()
65
-        )
66
-        .navigationBarTitle("Meter")
67
-        .navigationBarItems(trailing: HStack (spacing: 6) {
68
-            if meter.operationalState > .notPresent {
69
-                RSSIView(RSSI: meter.btSerial.RSSI)
70
-                    .frame(width: 18, height: 18)
71
-                    .padding(.leading, 6)
72
-                    .padding(.vertical)
73
-            }
74
-            NavigationLink(destination: MeterInfoView().environmentObject(meter)) {
75
-                Image(systemName: "info.circle.fill")
76
-                    .padding(.vertical)
88
+    }
89
+
90
+    private func landscapeDeck(size: CGSize) -> some View {
91
+        TabView {
92
+            landscapeFace {
93
+                if meter.operationalState == .dataIsAvailable {
94
+                    HStack(alignment: .top, spacing: 16) {
95
+                        connectionCard(compact: true)
96
+                            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
97
+
98
+                        landscapeActionPanel(size: size)
99
+                    }
100
+                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
101
+                } else {
102
+                    connectionCard(compact: true)
103
+                        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
104
+                }
77 105
             }
78
-            NavigationLink(destination: MeterSettingsView().environmentObject(meter)) {
79
-                Image(systemName: "gearshape.fill")
80
-                    .padding(.vertical)
106
+
107
+            if meter.operationalState == .dataIsAvailable {
108
+                landscapeFace {
109
+                    LiveView(compactLayout: true, availableSize: size)
110
+                        .padding(16)
111
+                        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
112
+                        .meterCard(tint: meter.color, fillOpacity: 0.12, strokeOpacity: 0.20)
113
+                }
114
+
115
+                if meter.measurements.power.context.isValid {
116
+                    landscapeFace {
117
+                        MeasurementChartView()
118
+                            .environmentObject(meter.measurements)
119
+                            .frame(height: max(250, size.height - 44))
120
+                            .padding(10)
121
+                            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
122
+                            .meterCard(tint: meter.color, fillOpacity: 0.12, strokeOpacity: 0.20)
123
+                    }
124
+                }
125
+
126
+                landscapeFace {
127
+                    ControlView(compactLayout: true, availableSize: size)
128
+                        .padding(16)
129
+                        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
130
+                        .meterCard(tint: .indigo, fillOpacity: 0.12, strokeOpacity: 0.20)
131
+                }
81 132
             }
82
-        })
133
+        }
134
+        .tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
135
+    }
136
+
137
+    private func landscapeFace<Content: View>(@ViewBuilder content: () -> Content) -> some View {
138
+        content()
139
+            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
140
+        .padding(.horizontal, 12)
141
+        .padding(.top, 10)
142
+        .padding(.bottom, 20)
143
+        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
144
+    }
145
+
146
+    private var meterBackground: some View {
147
+        LinearGradient(
148
+            colors: [
149
+                meter.color.opacity(0.22),
150
+                Color.secondary.opacity(0.08),
151
+                Color.clear
152
+            ],
153
+            startPoint: .topLeading,
154
+            endPoint: .bottomTrailing
155
+        )
156
+        .ignoresSafeArea()
83 157
     }
84 158
 
85
-    private var connectionCard: some View {
86
-        VStack(alignment: .leading, spacing: 18) {
159
+    private func isLandscape(size: CGSize) -> Bool {
160
+        size.width > size.height
161
+    }
162
+
163
+    private func connectionCard(compact: Bool = false) -> some View {
164
+        VStack(alignment: .leading, spacing: compact ? 12 : 18) {
87 165
             HStack(alignment: .top) {
88 166
                 VStack(alignment: .leading, spacing: 6) {
89 167
                     Text(meter.name)
90
-                        .font(.system(.title2, design: .rounded).weight(.bold))
168
+                        .font(.system(compact ? .title3 : .title2, design: .rounded).weight(.bold))
91 169
                     Text(meter.deviceModelSummary)
92 170
                         .font(.subheadline.weight(.semibold))
93 171
                         .foregroundColor(.secondary)
94 172
                 }
95 173
                 Spacer()
96
-                VStack(alignment: .trailing, spacing: 6) {
97
-                    Text(statusText)
98
-                        .font(.caption.weight(.bold))
99
-                        .padding(.horizontal, 12)
100
-                        .padding(.vertical, 6)
101
-                        .meterCard(tint: statusColor, fillOpacity: 0.24, strokeOpacity: 0.30, cornerRadius: 999)
102
-                    if meter.operationalState > .notPresent {
103
-                        Text("RSSI \(meter.btSerial.RSSI)")
104
-                            .font(.caption)
105
-                            .foregroundColor(.secondary)
174
+                Group {
175
+                    if compact {
176
+                        HStack(spacing: 8) {
177
+                            statusBadge
178
+                            if meter.operationalState > .notPresent {
179
+                                Text("RSSI \(meter.btSerial.RSSI)")
180
+                                    .font(.caption)
181
+                                    .foregroundColor(.secondary)
182
+                            }
183
+                        }
184
+                    } else {
185
+                        VStack(alignment: .trailing, spacing: 6) {
186
+                            statusBadge
187
+                            if meter.operationalState > .notPresent {
188
+                                Text("RSSI \(meter.btSerial.RSSI)")
189
+                                    .font(.caption)
190
+                                    .foregroundColor(.secondary)
191
+                            }
192
+                        }
106 193
                     }
107 194
                 }
108 195
             }
109 196
 
110
-            connectionActionArea
197
+            if compact {
198
+                Spacer(minLength: 0)
199
+            }
200
+
201
+            connectionActionArea(compact: compact)
111 202
         }
112
-        .padding(20)
203
+        .padding(compact ? 16 : 20)
204
+        .frame(maxWidth: .infinity, maxHeight: compact ? .infinity : nil, alignment: .topLeading)
113 205
         .meterCard(tint: meter.color, fillOpacity: 0.22, strokeOpacity: 0.24)
114 206
     }
115 207
 
116
-    private var actionGrid: some View {
117
-        GeometryReader { proxy in
208
+    private func actionGrid(compact: Bool = false, embedded: Bool = false) -> some View {
209
+        let currentActionHeight = compact ? CGFloat(86) : actionButtonHeight
210
+
211
+        return GeometryReader { proxy in
118 212
             let buttonWidth = actionButtonWidth(for: proxy.size.width)
119 213
             let stripWidth = actionStripWidth(for: buttonWidth)
214
+            let stripContent = HStack(spacing: 0) {
215
+                meterSheetButton(icon: "square.grid.2x2.fill", title: meter.dataGroupsTitle, tint: .teal, width: buttonWidth, height: currentActionHeight, compact: compact) {
216
+                    dataGroupsViewVisibility.toggle()
217
+                }
218
+                .sheet(isPresented: $dataGroupsViewVisibility) {
219
+                    DataGroupsView(visibility: $dataGroupsViewVisibility)
220
+                        .environmentObject(meter)
221
+                }
120 222
 
121
-            HStack {
122
-                Spacer(minLength: 0)
123
-                HStack(spacing: 0) {
124
-                    meterSheetButton(icon: "square.grid.2x2.fill", title: meter.dataGroupsTitle, tint: .teal, width: buttonWidth) {
125
-                        dataGroupsViewVisibility.toggle()
223
+                if meter.supportsRecordingView {
224
+                    actionStripDivider(height: currentActionHeight)
225
+                    meterSheetButton(icon: "gauge.with.dots.needle.50percent", title: "Charge Record", tint: .pink, width: buttonWidth, height: currentActionHeight, compact: compact) {
226
+                        recordingViewVisibility.toggle()
126 227
                     }
127
-                    .sheet(isPresented: $dataGroupsViewVisibility) {
128
-                        DataGroupsView(visibility: $dataGroupsViewVisibility)
228
+                    .sheet(isPresented: $recordingViewVisibility) {
229
+                        RecordingView(visibility: $recordingViewVisibility)
129 230
                             .environmentObject(meter)
130 231
                     }
232
+                }
131 233
 
132
-                    if meter.supportsRecordingView {
133
-                        actionStripDivider
134
-                        meterSheetButton(icon: "gauge.with.dots.needle.50percent", title: "Charge Record", tint: .pink, width: buttonWidth) {
135
-                            recordingViewVisibility.toggle()
136
-                        }
137
-                        .sheet(isPresented: $recordingViewVisibility) {
138
-                            RecordingView(visibility: $recordingViewVisibility)
139
-                                .environmentObject(meter)
140
-                        }
141
-                    }
234
+                actionStripDivider(height: currentActionHeight)
235
+                meterSheetButton(icon: "clock.arrow.circlepath", title: "App History", tint: .blue, width: buttonWidth, height: currentActionHeight, compact: compact) {
236
+                    measurementsViewVisibility.toggle()
237
+                }
238
+                .sheet(isPresented: $measurementsViewVisibility) {
239
+                    MeasurementsView(visibility: $measurementsViewVisibility)
240
+                        .environmentObject(meter.measurements)
241
+                }
242
+            }
243
+            .padding(actionStripPadding)
244
+            .frame(width: stripWidth)
142 245
 
143
-                    actionStripDivider
144
-                    meterSheetButton(icon: "clock.arrow.circlepath", title: "App History", tint: .blue, width: buttonWidth) {
145
-                        measurementsViewVisibility.toggle()
146
-                    }
147
-                    .sheet(isPresented: $measurementsViewVisibility) {
148
-                        MeasurementsView(visibility: $measurementsViewVisibility)
149
-                            .environmentObject(meter.measurements)
150
-                    }
246
+            HStack {
247
+                Spacer(minLength: 0)
248
+                if embedded {
249
+                    stripContent
250
+                } else {
251
+                    stripContent
252
+                        .meterCard(tint: Color.secondary, fillOpacity: 0.10, strokeOpacity: 0.16)
151 253
                 }
152
-                .padding(actionStripPadding)
153
-                .frame(width: stripWidth)
154
-                .meterCard(tint: Color.secondary, fillOpacity: 0.10, strokeOpacity: 0.16)
155 254
                 Spacer(minLength: 0)
156 255
             }
157 256
         }
158
-        .frame(height: actionButtonHeight + (actionStripPadding * 2))
257
+        .frame(height: currentActionHeight + (actionStripPadding * 2))
258
+    }
259
+
260
+    private func landscapeActionPanel(size: CGSize) -> some View {
261
+        VStack(alignment: .leading, spacing: 14) {
262
+            Text("Actions")
263
+                .font(.headline)
264
+
265
+            Spacer(minLength: 0)
266
+
267
+            actionGrid(compact: true, embedded: true)
268
+
269
+            Spacer(minLength: 0)
270
+        }
271
+        .padding(18)
272
+        .frame(width: min(420, max(320, size.width * 0.34)))
273
+        .frame(maxHeight: .infinity, alignment: .topLeading)
274
+        .meterCard(tint: Color.secondary, fillOpacity: 0.10, strokeOpacity: 0.16)
159 275
     }
160 276
     
161
-    private var connectionActionArea: some View {
277
+    private func connectionActionArea(compact: Bool = false) -> some View {
162 278
         let connected = meter.operationalState >= .peripheralConnectionPending
163 279
         let tint = connected ? disconnectActionTint : connectActionTint
164 280
 
@@ -171,7 +287,7 @@ struct MeterView: View {
171 287
                         .fontWeight(.semibold)
172 288
                     Spacer()
173 289
                 }
174
-                .padding(16)
290
+                .padding(compact ? 12 : 16)
175 291
                 .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.18)
176 292
             } else {
177 293
                 Button(action: {
@@ -192,7 +308,7 @@ struct MeterView: View {
192 308
                         Spacer()
193 309
                     }
194 310
                     .padding(.horizontal, 18)
195
-                    .padding(.vertical, 14)
311
+                    .padding(.vertical, compact ? 10 : 14)
196 312
                     .frame(maxWidth: .infinity)
197 313
                     .meterCard(tint: tint, fillOpacity: 0.14, strokeOpacity: 0.20)
198 314
                 }
@@ -201,21 +317,21 @@ struct MeterView: View {
201 317
         }
202 318
     }
203 319
 
204
-    fileprivate func meterSheetButton(icon: String, title: String, tint: Color, width: CGFloat, action: @escaping () -> Void) -> some View {
320
+    fileprivate func meterSheetButton(icon: String, title: String, tint: Color, width: CGFloat, height: CGFloat, compact: Bool = false, action: @escaping () -> Void) -> some View {
205 321
         Button(action: action) {
206
-            VStack(spacing: 10) {
322
+            VStack(spacing: compact ? 8 : 10) {
207 323
                 Image(systemName: icon)
208
-                    .font(.system(size: 20, weight: .semibold))
209
-                    .frame(width: 40, height: 40)
324
+                    .font(.system(size: compact ? 18 : 20, weight: .semibold))
325
+                    .frame(width: compact ? 34 : 40, height: compact ? 34 : 40)
210 326
                     .background(Circle().fill(tint.opacity(0.14)))
211 327
                 Text(title)
212
-                    .font(.footnote.weight(.semibold))
328
+                    .font((compact ? Font.caption : .footnote).weight(.semibold))
213 329
                     .multilineTextAlignment(.center)
214 330
                     .lineLimit(2)
215 331
                     .minimumScaleFactor(0.9)
216 332
             }
217 333
             .foregroundColor(tint)
218
-            .frame(width: width, height: actionButtonHeight)
334
+            .frame(width: width, height: height)
219 335
             .contentShape(Rectangle())
220 336
         }
221 337
         .buttonStyle(.plain)
@@ -237,10 +353,18 @@ struct MeterView: View {
237 353
         return (buttonWidth * visibleActionButtonCount) + dividerWidth + (actionStripPadding * 2)
238 354
     }
239 355
 
240
-    private var actionStripDivider: some View {
356
+    private func actionStripDivider(height: CGFloat) -> some View {
241 357
         Rectangle()
242 358
             .fill(Color.secondary.opacity(0.16))
243
-            .frame(width: actionDividerWidth, height: actionButtonHeight - 24)
359
+            .frame(width: actionDividerWidth, height: max(44, height - 22))
360
+    }
361
+
362
+    private var statusBadge: some View {
363
+        Text(statusText)
364
+            .font(.caption.weight(.bold))
365
+            .padding(.horizontal, 12)
366
+            .padding(.vertical, 6)
367
+            .meterCard(tint: statusColor, fillOpacity: 0.24, strokeOpacity: 0.30, cornerRadius: 999)
244 368
     }
245 369
 
246 370
     private var connectActionTint: Color {