Showing 24 changed files with 1312 additions and 774 deletions
+33 -0
Documentation/Project Structure and Naming.md
@@ -90,6 +90,39 @@ Views/Meter/
90 90
         ScreenTimeoutEditorView.swift
91 91
 ```
92 92
 
93
+## Current Charged Device Pattern
94
+
95
+Use the same root categories as Meter work: shared components first, screen/detail roots next, then sheets and sidebar-specific views.
96
+
97
+```text
98
+Views/ChargedDevices/
99
+  Components/
100
+    ChargedDeviceDetailTabBarView.swift
101
+    ChargedDeviceEditorScaffoldView.swift
102
+    ChargedDeviceIdentityViews.swift
103
+    ChargedDeviceLibraryRowView.swift
104
+    ChargedDeviceQRCodeView.swift
105
+    ChargedDeviceSidebarCardView.swift
106
+  Details/
107
+    ChargedDeviceDetailView.swift
108
+  Sessions/
109
+    ChargedDeviceActiveSessionView.swift
110
+    ChargedDeviceSessionDetailView.swift
111
+    ChargedDeviceSessionsView.swift
112
+  Sheets/
113
+    ChargeSession/
114
+      BatteryCheckpointEditorSheetView.swift
115
+      ChargeSessionCompletionSheetView.swift
116
+    Editors/
117
+      ChargedDeviceEditorSheetView.swift
118
+      ChargerEditorSheetView.swift
119
+    Library/
120
+      ChargedDeviceLibrarySheetView.swift
121
+  Sidebar/
122
+    SidebarChargedDeviceLibraryView.swift
123
+    SidebarChargedDevicesSectionView.swift
124
+```
125
+
93 126
 ## Refactor Examples
94 127
 
95 128
 - `Connection/` -> `Home/`
+97 -5
USB Meter.xcodeproj/project.pbxproj
@@ -11,6 +11,7 @@
11 11
 		4308CF8624176CAB0002E80B /* DataGroupRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4308CF8524176CAB0002E80B /* DataGroupRowView.swift */; };
12 12
 		4308CF882417770D0002E80B /* DataGroupsSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4308CF872417770D0002E80B /* DataGroupsSheetView.swift */; };
13 13
 		430CB4FC245E07EB006525C2 /* ChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430CB4FB245E07EB006525C2 /* ChevronView.swift */; };
14
+		430CB4FD245E07EB006525C3 /* AdaptiveTabBarPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430CB4FE245E07EB006525C4 /* AdaptiveTabBarPresentation.swift */; };
14 15
 		4311E63A241384960080EA59 /* DeviceHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4311E639241384960080EA59 /* DeviceHelpView.swift */; };
15 16
 		4327461B24619CED0009BE4B /* MeterRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4327461A24619CED0009BE4B /* MeterRowView.swift */; };
16 17
 		432EA6442445A559006FC905 /* ChartContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 432EA6432445A559006FC905 /* ChartContext.swift */; };
@@ -70,6 +71,12 @@
70 71
 		C10000073C8E4A7A00A10007 /* SidebarChargedDevicesSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000173C8E4A7A00A10017 /* SidebarChargedDevicesSectionView.swift */; };
71 72
 		C10000083C8E4A7A00A10008 /* BatteryCheckpointEditorSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000183C8E4A7A00A10018 /* BatteryCheckpointEditorSheetView.swift */; };
72 73
 		CE00CE013C8E4A7A00CE00CE /* ChargerEditorSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE00CE023C8E4A7A00CE00CE /* ChargerEditorSheetView.swift */; };
74
+		CD0002013FA0000000000001 /* ChargedDeviceIdentityViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0001013FA0000000000001 /* ChargedDeviceIdentityViews.swift */; };
75
+		CD0002023FA0000000000002 /* ChargedDeviceLibraryRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0001023FA0000000000002 /* ChargedDeviceLibraryRowView.swift */; };
76
+		CD0002033FA0000000000003 /* ChargedDeviceSidebarCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0001033FA0000000000003 /* ChargedDeviceSidebarCardView.swift */; };
77
+		CD0002043FA0000000000004 /* ChargedDeviceEditorScaffoldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0001043FA0000000000004 /* ChargedDeviceEditorScaffoldView.swift */; };
78
+		CD0002053FA0000000000005 /* SidebarChargedDeviceLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0001053FA0000000000005 /* SidebarChargedDeviceLibraryView.swift */; };
79
+		CD0002063FA0000000000006 /* ChargedDeviceDetailTabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0001063FA0000000000006 /* ChargedDeviceDetailTabBarView.swift */; };
73 80
 		C10000093C8E4A7A00A10009 /* CKModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C10000213C8E4A7A00A10021 /* CKModel.xcdatamodeld */; };
74 81
 		D28F11013C8E4A7A00A10011 /* MeterHomeTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11023C8E4A7A00A10012 /* MeterHomeTabView.swift */; };
75 82
 		D28F11033C8E4A7A00A10013 /* MeterLiveTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11043C8E4A7A00A10014 /* MeterLiveTabView.swift */; };
@@ -128,6 +135,7 @@
128 135
 		4308CF8524176CAB0002E80B /* DataGroupRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGroupRowView.swift; sourceTree = "<group>"; };
129 136
 		4308CF872417770D0002E80B /* DataGroupsSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGroupsSheetView.swift; sourceTree = "<group>"; };
130 137
 		430CB4FB245E07EB006525C2 /* ChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronView.swift; sourceTree = "<group>"; };
138
+		430CB4FE245E07EB006525C4 /* AdaptiveTabBarPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveTabBarPresentation.swift; sourceTree = "<group>"; };
131 139
 		4311E639241384960080EA59 /* DeviceHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceHelpView.swift; sourceTree = "<group>"; };
132 140
 		4327461A24619CED0009BE4B /* MeterRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterRowView.swift; sourceTree = "<group>"; };
133 141
 		432EA6432445A559006FC905 /* ChartContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartContext.swift; sourceTree = "<group>"; };
@@ -194,6 +202,12 @@
194 202
 		C10000173C8E4A7A00A10017 /* SidebarChargedDevicesSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarChargedDevicesSectionView.swift; sourceTree = "<group>"; };
195 203
 		C10000183C8E4A7A00A10018 /* BatteryCheckpointEditorSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryCheckpointEditorSheetView.swift; sourceTree = "<group>"; };
196 204
 		CE00CE023C8E4A7A00CE00CE /* ChargerEditorSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargerEditorSheetView.swift; sourceTree = "<group>"; };
205
+		CD0001013FA0000000000001 /* ChargedDeviceIdentityViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargedDeviceIdentityViews.swift; sourceTree = "<group>"; };
206
+		CD0001023FA0000000000002 /* ChargedDeviceLibraryRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargedDeviceLibraryRowView.swift; sourceTree = "<group>"; };
207
+		CD0001033FA0000000000003 /* ChargedDeviceSidebarCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargedDeviceSidebarCardView.swift; sourceTree = "<group>"; };
208
+		CD0001043FA0000000000004 /* ChargedDeviceEditorScaffoldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargedDeviceEditorScaffoldView.swift; sourceTree = "<group>"; };
209
+		CD0001053FA0000000000005 /* SidebarChargedDeviceLibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarChargedDeviceLibraryView.swift; sourceTree = "<group>"; };
210
+		CD0001063FA0000000000006 /* ChargedDeviceDetailTabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargedDeviceDetailTabBarView.swift; sourceTree = "<group>"; };
197 211
 		C10000193C8E4A7A00A10019 /* USB_Meter 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "USB_Meter 4.xcdatamodel"; sourceTree = "<group>"; };
198 212
 		C100001A3C8E4A7A00A1001A /* USB_Meter 5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "USB_Meter 5.xcdatamodel"; sourceTree = "<group>"; };
199 213
 		C100001B3C8E4A7A00A1001B /* USB_Meter 6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "USB_Meter 6.xcdatamodel"; sourceTree = "<group>"; };
@@ -503,21 +517,91 @@
503 517
 			sourceTree = "<group>";
504 518
 		};
505 519
 		C10000203C8E4A7A00A10020 /* ChargedDevices */ = {
520
+			isa = PBXGroup;
521
+			children = (
522
+				CD0000103FA0000000000010 /* Components */,
523
+				CD0000113FA0000000000011 /* Details */,
524
+				CD0000123FA0000000000012 /* Sessions */,
525
+				CD0000133FA0000000000013 /* Sheets */,
526
+				CD0000173FA0000000000017 /* Sidebar */,
527
+			);
528
+			path = ChargedDevices;
529
+			sourceTree = "<group>";
530
+		};
531
+		CD0000103FA0000000000010 /* Components */ = {
532
+			isa = PBXGroup;
533
+			children = (
534
+				CD0001063FA0000000000006 /* ChargedDeviceDetailTabBarView.swift */,
535
+				CD0001043FA0000000000004 /* ChargedDeviceEditorScaffoldView.swift */,
536
+				CD0001013FA0000000000001 /* ChargedDeviceIdentityViews.swift */,
537
+				CD0001023FA0000000000002 /* ChargedDeviceLibraryRowView.swift */,
538
+				C10000133C8E4A7A00A10013 /* ChargedDeviceQRCodeView.swift */,
539
+				CD0001033FA0000000000003 /* ChargedDeviceSidebarCardView.swift */,
540
+			);
541
+			path = Components;
542
+			sourceTree = "<group>";
543
+		};
544
+		CD0000113FA0000000000011 /* Details */ = {
506 545
 			isa = PBXGroup;
507 546
 			children = (
508 547
 				C10000163C8E4A7A00A10016 /* ChargedDeviceDetailView.swift */,
548
+			);
549
+			path = Details;
550
+			sourceTree = "<group>";
551
+		};
552
+		CD0000123FA0000000000012 /* Sessions */ = {
553
+			isa = PBXGroup;
554
+			children = (
509 555
 				C1A500023C9D000100A10002 /* ChargedDeviceActiveSessionView.swift */,
510
-				C100001A3C8E4A7A00A1002A /* ChargedDeviceSessionsView.swift */,
511 556
 				C100001B3C8E4A7A00A1002B /* ChargedDeviceSessionDetailView.swift */,
557
+				C100001A3C8E4A7A00A1002A /* ChargedDeviceSessionsView.swift */,
558
+			);
559
+			path = Sessions;
560
+			sourceTree = "<group>";
561
+		};
562
+		CD0000133FA0000000000013 /* Sheets */ = {
563
+			isa = PBXGroup;
564
+			children = (
565
+				CD0000163FA0000000000016 /* ChargeSession */,
566
+				CD0000143FA0000000000014 /* Editors */,
567
+				CD0000153FA0000000000015 /* Library */,
568
+			);
569
+			path = Sheets;
570
+			sourceTree = "<group>";
571
+		};
572
+		CD0000143FA0000000000014 /* Editors */ = {
573
+			isa = PBXGroup;
574
+			children = (
512 575
 				C10000143C8E4A7A00A10014 /* ChargedDeviceEditorSheetView.swift */,
513
-				C1CCSS023C8E4A7A00CCSS02 /* ChargeSessionCompletionSheetView.swift */,
576
+				CE00CE023C8E4A7A00CE00CE /* ChargerEditorSheetView.swift */,
577
+			);
578
+			path = Editors;
579
+			sourceTree = "<group>";
580
+		};
581
+		CD0000153FA0000000000015 /* Library */ = {
582
+			isa = PBXGroup;
583
+			children = (
514 584
 				C10000153C8E4A7A00A10015 /* ChargedDeviceLibrarySheetView.swift */,
515
-				C10000133C8E4A7A00A10013 /* ChargedDeviceQRCodeView.swift */,
585
+			);
586
+			path = Library;
587
+			sourceTree = "<group>";
588
+		};
589
+		CD0000163FA0000000000016 /* ChargeSession */ = {
590
+			isa = PBXGroup;
591
+			children = (
516 592
 				C10000183C8E4A7A00A10018 /* BatteryCheckpointEditorSheetView.swift */,
517
-				CE00CE023C8E4A7A00CE00CE /* ChargerEditorSheetView.swift */,
593
+				C1CCSS023C8E4A7A00CCSS02 /* ChargeSessionCompletionSheetView.swift */,
594
+			);
595
+			path = ChargeSession;
596
+			sourceTree = "<group>";
597
+		};
598
+		CD0000173FA0000000000017 /* Sidebar */ = {
599
+			isa = PBXGroup;
600
+			children = (
601
+				CD0001053FA0000000000005 /* SidebarChargedDeviceLibraryView.swift */,
518 602
 				C10000173C8E4A7A00A10017 /* SidebarChargedDevicesSectionView.swift */,
519 603
 			);
520
-			path = ChargedDevices;
604
+			path = Sidebar;
521 605
 			sourceTree = "<group>";
522 606
 		};
523 607
 		D28F10013C8E4A7A00A10001 /* Sheets */ = {
@@ -550,6 +634,7 @@
550 634
 		D28F10033C8E4A7A00A10003 /* Generic */ = {
551 635
 			isa = PBXGroup;
552 636
 			children = (
637
+				430CB4FE245E07EB006525C4 /* AdaptiveTabBarPresentation.swift */,
553 638
 				4360A34C241CBB3800B464F9 /* RSSIView.swift */,
554 639
 				430CB4FB245E07EB006525C2 /* ChevronView.swift */,
555 640
 			);
@@ -820,10 +905,17 @@
820 905
 				C10000073C8E4A7A00A10007 /* SidebarChargedDevicesSectionView.swift in Sources */,
821 906
 				C10000083C8E4A7A00A10008 /* BatteryCheckpointEditorSheetView.swift in Sources */,
822 907
 				CE00CE013C8E4A7A00CE00CE /* ChargerEditorSheetView.swift in Sources */,
908
+				CD0002013FA0000000000001 /* ChargedDeviceIdentityViews.swift in Sources */,
909
+				CD0002023FA0000000000002 /* ChargedDeviceLibraryRowView.swift in Sources */,
910
+				CD0002033FA0000000000003 /* ChargedDeviceSidebarCardView.swift in Sources */,
911
+				CD0002043FA0000000000004 /* ChargedDeviceEditorScaffoldView.swift in Sources */,
912
+				CD0002053FA0000000000005 /* SidebarChargedDeviceLibraryView.swift in Sources */,
913
+				CD0002063FA0000000000006 /* ChargedDeviceDetailTabBarView.swift in Sources */,
823 914
 				C10000093C8E4A7A00A10009 /* CKModel.xcdatamodeld in Sources */,
824 915
 				B0A000113C8F000100A10011 /* ChargerStandbyPowerStore.swift in Sources */,
825 916
 				B0A000123C8F000100A10012 /* ChargerStandbyPowerWizardView.swift in Sources */,
826 917
 				4360A34D241CBB3800B464F9 /* RSSIView.swift in Sources */,
918
+				430CB4FD245E07EB006525C3 /* AdaptiveTabBarPresentation.swift in Sources */,
827 919
 				437D47D12415F91B00B7768E /* MeterLiveContentView.swift in Sources */,
828 920
 				4383B465240EB6B200DAAEBF /* UserDefault.swift in Sources */,
829 921
 				3407A133FADB8858DC2A1FED /* MeterNameStore.swift in Sources */,
+34 -0
USB Meter/Model/ChargeInsightsModel.swift
@@ -114,6 +114,40 @@ enum ChargedDeviceClass: String, CaseIterable, Identifiable, Codable {
114 114
         }
115 115
     }
116 116
 
117
+    var defaultChargingSupport: (wired: Bool, wireless: Bool) {
118
+        if let enforcedChargingSupport {
119
+            return enforcedChargingSupport
120
+        }
121
+
122
+        switch self {
123
+        case .iphone:
124
+            return (wired: true, wireless: true)
125
+        case .watch:
126
+            return (wired: false, wireless: true)
127
+        case .powerbank:
128
+            return (wired: true, wireless: false)
129
+        case .charger:
130
+            return (wired: false, wireless: true)
131
+        case .other:
132
+            return (wired: true, wireless: false)
133
+        }
134
+    }
135
+
136
+    var defaultChargingStateAvailability: ChargingStateAvailability {
137
+        enforcedChargingStateAvailability ?? {
138
+            switch self {
139
+            case .iphone:
140
+                return .onOrOff
141
+            case .watch:
142
+                return .onOnly
143
+            case .powerbank:
144
+                return .offOnly
145
+            case .charger, .other:
146
+                return .onOrOff
147
+            }
148
+        }()
149
+    }
150
+
117 151
     func normalizedChargingSupport(
118 152
         supportsWiredCharging: Bool,
119 153
         supportsWirelessCharging: Bool
+0 -520
USB Meter/Views/ChargedDevices/ChargedDeviceLibrarySheetView.swift
@@ -1,520 +0,0 @@
1
-//
2
-//  ChargedDeviceLibrarySheetView.swift
3
-//  USB Meter
4
-//
5
-//  Created by Codex on 10/04/2026.
6
-//
7
-
8
-import SwiftUI
9
-import UIKit
10
-
11
-enum ChargedDeviceLibraryMode {
12
-    case device
13
-    case charger
14
-
15
-    var kind: ChargedDeviceKind {
16
-        switch self {
17
-        case .device:
18
-            return .device
19
-        case .charger:
20
-            return .charger
21
-        }
22
-    }
23
-
24
-    var title: String {
25
-        switch self {
26
-        case .device:
27
-            return "Devices"
28
-        case .charger:
29
-            return "Chargers"
30
-        }
31
-    }
32
-
33
-    var singularTitle: String {
34
-        switch self {
35
-        case .device:
36
-            return "Device"
37
-        case .charger:
38
-            return "Charger"
39
-        }
40
-    }
41
-}
42
-
43
-struct ChargedDeviceLibrarySheetView: View {
44
-    @EnvironmentObject private var appData: AppData
45
-    @Environment(\.dismiss) private var dismiss
46
-
47
-    let meterMACAddress: String
48
-    let meterTint: Color
49
-    let mode: ChargedDeviceLibraryMode
50
-    /// true = standalone sheet with own NavigationView; false = pushed into parent nav stack
51
-    let standalone: Bool
52
-
53
-    @State private var showingNewEditor = false
54
-    @State private var editingChargedDevice: ChargedDeviceSummary?
55
-    @State private var pendingDeletion: ChargedDeviceSummary?
56
-
57
-    init(
58
-        meterMACAddress: String,
59
-        meterTint: Color,
60
-        mode: ChargedDeviceLibraryMode,
61
-        standalone: Bool = true
62
-    ) {
63
-        self.meterMACAddress = meterMACAddress
64
-        self.meterTint = meterTint
65
-        self.mode = mode
66
-        self.standalone = standalone
67
-    }
68
-
69
-    var body: some View {
70
-        if standalone {
71
-            NavigationView { listContent }
72
-                .navigationViewStyle(StackNavigationViewStyle())
73
-        } else {
74
-            listContent
75
-        }
76
-    }
77
-
78
-    private var listContent: some View {
79
-        List {
80
-            if displayedChargedDevices.isEmpty {
81
-                VStack(alignment: .leading, spacing: 10) {
82
-                    HStack(spacing: 8) {
83
-                        Text("No \(mode.title.lowercased()) yet.")
84
-                            .font(.headline)
85
-                        ContextInfoButton(
86
-                            title: mode.title,
87
-                            message: emptyStateDescription
88
-                        )
89
-                    }
90
-                }
91
-                .padding(.vertical, 10)
92
-                .listRowBackground(Color.clear)
93
-            } else {
94
-                ForEach(displayedChargedDevices) { chargedDevice in
95
-                    Button {
96
-                        select(chargedDevice)
97
-                        dismiss()
98
-                    } label: {
99
-                        ChargedDeviceLibraryRowView(
100
-                            chargedDevice: chargedDevice,
101
-                            isSelected: chargedDevice.id == selectedDeviceID
102
-                        )
103
-                    }
104
-                    .buttonStyle(.plain)
105
-                    .swipeActions(edge: .trailing, allowsFullSwipe: false) {
106
-                        Button(role: .destructive) {
107
-                            pendingDeletion = chargedDevice
108
-                        } label: {
109
-                            Label("Delete", systemImage: "trash")
110
-                        }
111
-                        Button {
112
-                            editingChargedDevice = chargedDevice
113
-                        } label: {
114
-                            Label("Edit", systemImage: "pencil")
115
-                        }
116
-                        .tint(.blue)
117
-                    }
118
-                    .contextMenu {
119
-                        Button {
120
-                            editingChargedDevice = chargedDevice
121
-                        } label: {
122
-                            Label("Edit \(mode.singularTitle)", systemImage: "pencil")
123
-                        }
124
-                        Button(role: .destructive) {
125
-                            pendingDeletion = chargedDevice
126
-                        } label: {
127
-                            Label("Delete \(mode.singularTitle)", systemImage: "trash")
128
-                        }
129
-                    }
130
-                }
131
-            }
132
-        }
133
-        .listStyle(InsetGroupedListStyle())
134
-        .background(
135
-            LinearGradient(
136
-                colors: [meterTint.opacity(0.14), Color.clear],
137
-                startPoint: .topLeading,
138
-                endPoint: .bottomTrailing
139
-            )
140
-            .ignoresSafeArea()
141
-        )
142
-        .navigationTitle(mode.title)
143
-        .navigationBarTitleDisplayMode(.inline)
144
-        .toolbar {
145
-            ToolbarItem(placement: .cancellationAction) {
146
-                if standalone {
147
-                    Button("Done") { dismiss() }
148
-                }
149
-            }
150
-            ToolbarItem(placement: .confirmationAction) {
151
-                Button("New") { showingNewEditor = true }
152
-            }
153
-        }
154
-        .sheet(isPresented: $showingNewEditor) {
155
-            newEditorSheet
156
-        }
157
-        .sheet(item: $editingChargedDevice) { device in
158
-            editEditorSheet(device)
159
-        }
160
-        .confirmationDialog(
161
-            "Delete \(pendingDeletion?.name ?? mode.singularTitle)?",
162
-            isPresented: Binding(
163
-                get: { pendingDeletion != nil },
164
-                set: { if !$0 { pendingDeletion = nil } }
165
-            ),
166
-            titleVisibility: .visible
167
-        ) {
168
-            Button("Delete", role: .destructive) {
169
-                if let device = pendingDeletion {
170
-                    _ = appData.deleteChargedDevice(id: device.id)
171
-                    pendingDeletion = nil
172
-                }
173
-            }
174
-            Button("Cancel", role: .cancel) { pendingDeletion = nil }
175
-        } message: {
176
-            Text("This will permanently remove the \(mode.singularTitle.lowercased()) and all associated data.")
177
-        }
178
-    }
179
-
180
-    @ViewBuilder
181
-    private var newEditorSheet: some View {
182
-        if mode == .charger {
183
-            ChargerEditorSheetView(
184
-                appData: appData,
185
-                meterMACAddress: meterMACAddress
186
-            )
187
-        } else {
188
-            ChargedDeviceEditorSheetView(meterMACAddress: meterMACAddress)
189
-                .environmentObject(appData)
190
-        }
191
-    }
192
-
193
-    @ViewBuilder
194
-    private func editEditorSheet(_ chargedDevice: ChargedDeviceSummary) -> some View {
195
-        if chargedDevice.isCharger {
196
-            ChargerEditorSheetView(
197
-                appData: appData,
198
-                chargedDevice: chargedDevice
199
-            )
200
-        } else {
201
-            ChargedDeviceEditorSheetView(
202
-                meterMACAddress: nil,
203
-                chargedDevice: chargedDevice
204
-            )
205
-            .environmentObject(appData)
206
-        }
207
-    }
208
-
209
-    private var displayedChargedDevices: [ChargedDeviceSummary] {
210
-        switch mode {
211
-        case .device:
212
-            return appData.deviceSummaries
213
-        case .charger:
214
-            return appData.chargerSummaries
215
-        }
216
-    }
217
-
218
-    private var selectedDeviceID: UUID? {
219
-        switch mode {
220
-        case .device:
221
-            return appData.currentChargedDeviceSummary(for: meterMACAddress)?.id
222
-        case .charger:
223
-            return appData.currentChargerSummary(for: meterMACAddress)?.id
224
-        }
225
-    }
226
-
227
-    private var emptyStateDescription: String {
228
-        switch mode {
229
-        case .device:
230
-            return "Create one here, then select it before or during a charging session. The selected device becomes the default for this meter."
231
-        case .charger:
232
-            return "Create one here, then select it for wireless charging sessions. The selected charger becomes the default wireless source for this meter."
233
-        }
234
-    }
235
-
236
-    private func select(_ chargedDevice: ChargedDeviceSummary) {
237
-        switch mode {
238
-        case .device:
239
-            appData.assignChargedDevice(chargedDevice.id, to: meterMACAddress)
240
-        case .charger:
241
-            appData.assignCharger(chargedDevice.id, to: meterMACAddress)
242
-        }
243
-    }
244
-}
245
-
246
-private struct ChargedDeviceLibraryRowView: View {
247
-    let chargedDevice: ChargedDeviceSummary
248
-    let isSelected: Bool
249
-
250
-    var body: some View {
251
-        HStack(alignment: .top, spacing: 14) {
252
-            ChargedDeviceQRCodeView(qrIdentifier: chargedDevice.qrIdentifier, side: 58)
253
-
254
-            VStack(alignment: .leading, spacing: 6) {
255
-                HStack {
256
-                    ChargedDeviceIdentityLabelView(
257
-                        chargedDevice: chargedDevice,
258
-                        iconPointSize: 17
259
-                    )
260
-                    .font(.headline)
261
-                    .foregroundColor(.primary)
262
-                    Spacer()
263
-                    if isSelected {
264
-                        Image(systemName: "checkmark.circle.fill")
265
-                            .foregroundColor(.green)
266
-                    }
267
-                }
268
-
269
-                Text(chargedDevice.identityTitle)
270
-                    .font(.caption.weight(.semibold))
271
-                    .foregroundColor(.secondary)
272
-
273
-                if chargedDevice.isCharger {
274
-                    if !chargedDevice.chargerObservedVoltageSelections.isEmpty {
275
-                        Text(
276
-                            chargedDevice.chargerObservedVoltageSelections
277
-                                .map { "\($0.format(decimalDigits: 1)) V" }
278
-                                .joined(separator: ", ")
279
-                        )
280
-                        .font(.caption2)
281
-                        .foregroundColor(.secondary)
282
-                    } else if let chargerMaximumPowerWatts = chargedDevice.chargerMaximumPowerWatts {
283
-                        Text("Max power: \(chargerMaximumPowerWatts.format(decimalDigits: 2)) W")
284
-                            .font(.caption2)
285
-                            .foregroundColor(.secondary)
286
-                    } else {
287
-                        Text("Wireless charger")
288
-                            .font(.caption2)
289
-                            .foregroundColor(.secondary)
290
-                    }
291
-                } else {
292
-                    Text(chargedDevice.supportedChargingModes.map(\.title).joined(separator: " + "))
293
-                        .font(.caption2)
294
-                        .foregroundColor(.secondary)
295
-
296
-                    if let capacity = chargedDevice.estimatedBatteryCapacityWh {
297
-                        Text("Estimated capacity: \(capacity.format(decimalDigits: 2)) Wh")
298
-                            .font(.caption)
299
-                            .foregroundColor(.secondary)
300
-                    }
301
-
302
-                    if let minimumCurrent = chargedDevice.minimumCurrentAmps {
303
-                        Text("Completion current: \(minimumCurrent.format(decimalDigits: 2)) A")
304
-                            .font(.caption2)
305
-                            .foregroundColor(.secondary)
306
-                    }
307
-                }
308
-            }
309
-        }
310
-        .padding(.vertical, 4)
311
-    }
312
-}
313
-
314
-struct ChargedDeviceIdentityLabelView: View {
315
-    let chargedDevice: ChargedDeviceSummary
316
-    var iconPointSize: CGFloat = 15
317
-
318
-    var body: some View {
319
-        HStack(alignment: .firstTextBaseline, spacing: 8) {
320
-            ChargedDeviceTemplateIconView(
321
-                icon: chargedDevice.identityIcon,
322
-                fallbackSystemName: chargedDevice.fallbackIdentitySymbolName,
323
-                pointSize: iconPointSize
324
-            )
325
-            Text(chargedDevice.name)
326
-        }
327
-    }
328
-}
329
-
330
-struct ChargedDeviceTemplateLabelView: View {
331
-    let template: ChargedDeviceTemplateDefinition
332
-    var iconPointSize: CGFloat = 15
333
-
334
-    var body: some View {
335
-        HStack(alignment: .firstTextBaseline, spacing: 8) {
336
-            ChargedDeviceTemplateIconView(
337
-                icon: template.icon,
338
-                fallbackSystemName: template.deviceClass.symbolName,
339
-                pointSize: iconPointSize
340
-            )
341
-            Text(template.name)
342
-        }
343
-    }
344
-}
345
-
346
-struct ChargedDeviceTemplateIconView: View {
347
-    let icon: ChargedDeviceTemplateIcon
348
-    let fallbackSystemName: String
349
-    var pointSize: CGFloat = 15
350
-
351
-    var body: some View {
352
-        Group {
353
-            if let assetName = resolvedAssetName {
354
-                Image(assetName)
355
-                    .renderingMode(.template)
356
-                    .resizable()
357
-                    .scaledToFit()
358
-            } else {
359
-                Image(systemName: resolvedSystemSymbolName)
360
-                    .font(.system(size: pointSize))
361
-            }
362
-        }
363
-        .frame(width: pointSize + 2, height: pointSize + 2)
364
-    }
365
-
366
-    private var resolvedAssetName: String? {
367
-        guard icon.type == .asset, UIImage(named: icon.name) != nil else {
368
-            return nil
369
-        }
370
-        return icon.name
371
-    }
372
-
373
-    private var resolvedSystemSymbolName: String {
374
-        let candidate = icon.resolvedSystemSymbolName(fallbackSystemName: fallbackSystemName)
375
-        if UIImage(systemName: candidate) != nil {
376
-            return candidate
377
-        }
378
-
379
-        if let fallbackSystemName = icon.fallbackSystemName,
380
-           UIImage(systemName: fallbackSystemName) != nil {
381
-            return fallbackSystemName
382
-        }
383
-
384
-        return fallbackSystemName
385
-    }
386
-}
387
-
388
-// MARK: - Sidebar Library View
389
-
390
-/// Full-management library for the sidebar — navigates into ChargedDeviceDetailView
391
-/// instead of select-and-dismiss like ChargedDeviceLibrarySheetView.
392
-struct SidebarChargedDeviceLibraryView: View {
393
-    @EnvironmentObject private var appData: AppData
394
-
395
-    let mode: ChargedDeviceLibraryMode
396
-
397
-    @State private var showingNewEditor = false
398
-    @State private var editingChargedDevice: ChargedDeviceSummary?
399
-    @State private var pendingDeletion: ChargedDeviceSummary?
400
-
401
-    private var tint: Color {
402
-        mode == .device ? .orange : .pink
403
-    }
404
-
405
-    var body: some View {
406
-        List {
407
-            if displayedDevices.isEmpty {
408
-                VStack(alignment: .leading, spacing: 10) {
409
-                    HStack(spacing: 8) {
410
-                        Text("No \(mode.title.lowercased()) yet.")
411
-                            .font(.headline)
412
-                        ContextInfoButton(
413
-                            title: mode.title,
414
-                            message: emptyStateDescription
415
-                        )
416
-                    }
417
-                }
418
-                .padding(.vertical, 10)
419
-                .listRowBackground(Color.clear)
420
-            } else {
421
-                ForEach(displayedDevices) { device in
422
-                    NavigationLink(destination: ChargedDeviceDetailView(chargedDeviceID: device.id)) {
423
-                        ChargedDeviceLibraryRowView(chargedDevice: device, isSelected: false)
424
-                    }
425
-                    .swipeActions(edge: .trailing, allowsFullSwipe: false) {
426
-                        Button(role: .destructive) {
427
-                            pendingDeletion = device
428
-                        } label: {
429
-                            Label("Delete", systemImage: "trash")
430
-                        }
431
-                        Button {
432
-                            editingChargedDevice = device
433
-                        } label: {
434
-                            Label("Edit", systemImage: "pencil")
435
-                        }
436
-                        .tint(.blue)
437
-                    }
438
-                    .contextMenu {
439
-                        Button {
440
-                            editingChargedDevice = device
441
-                        } label: {
442
-                            Label("Edit \(mode.singularTitle)", systemImage: "pencil")
443
-                        }
444
-                        Button(role: .destructive) {
445
-                            pendingDeletion = device
446
-                        } label: {
447
-                            Label("Delete \(mode.singularTitle)", systemImage: "trash")
448
-                        }
449
-                    }
450
-                }
451
-            }
452
-        }
453
-        .listStyle(InsetGroupedListStyle())
454
-        .background(
455
-            LinearGradient(
456
-                colors: [tint.opacity(0.14), Color.clear],
457
-                startPoint: .topLeading,
458
-                endPoint: .bottomTrailing
459
-            )
460
-            .ignoresSafeArea()
461
-        )
462
-        .navigationTitle(mode.title)
463
-        .navigationBarTitleDisplayMode(.inline)
464
-        .toolbar {
465
-            ToolbarItem(placement: .primaryAction) {
466
-                Button("New") { showingNewEditor = true }
467
-            }
468
-        }
469
-        .sheet(isPresented: $showingNewEditor) { newEditorSheet }
470
-        .sheet(item: $editingChargedDevice) { device in editEditorSheet(device) }
471
-        .confirmationDialog(
472
-            "Delete \(pendingDeletion?.name ?? mode.singularTitle)?",
473
-            isPresented: Binding(
474
-                get: { pendingDeletion != nil },
475
-                set: { if !$0 { pendingDeletion = nil } }
476
-            ),
477
-            titleVisibility: .visible
478
-        ) {
479
-            Button("Delete", role: .destructive) {
480
-                if let device = pendingDeletion {
481
-                    _ = appData.deleteChargedDevice(id: device.id)
482
-                    pendingDeletion = nil
483
-                }
484
-            }
485
-            Button("Cancel", role: .cancel) { pendingDeletion = nil }
486
-        } message: {
487
-            Text("This will permanently remove the \(mode.singularTitle.lowercased()) and all associated data.")
488
-        }
489
-    }
490
-
491
-    private var displayedDevices: [ChargedDeviceSummary] {
492
-        mode == .device ? appData.deviceSummaries : appData.chargerSummaries
493
-    }
494
-
495
-    @ViewBuilder
496
-    private var newEditorSheet: some View {
497
-        if mode == .charger {
498
-            ChargerEditorSheetView(appData: appData, meterMACAddress: nil)
499
-        } else {
500
-            ChargedDeviceEditorSheetView(meterMACAddress: nil)
501
-                .environmentObject(appData)
502
-        }
503
-    }
504
-
505
-    @ViewBuilder
506
-    private func editEditorSheet(_ device: ChargedDeviceSummary) -> some View {
507
-        if device.isCharger {
508
-            ChargerEditorSheetView(appData: appData, chargedDevice: device)
509
-        } else {
510
-            ChargedDeviceEditorSheetView(meterMACAddress: nil, chargedDevice: device)
511
-                .environmentObject(appData)
512
-        }
513
-    }
514
-
515
-    private var emptyStateDescription: String {
516
-        mode == .device
517
-            ? "Create one here, then select it before or during a charging session. The selected device becomes the default for this meter."
518
-            : "Create one here, then select it for wireless charging sessions. The selected charger becomes the default wireless source for this meter."
519
-    }
520
-}
+82 -0
USB Meter/Views/ChargedDevices/Components/ChargedDeviceDetailTabBarView.swift
@@ -0,0 +1,82 @@
1
+//
2
+//  ChargedDeviceDetailTabBarView.swift
3
+//  USB Meter
4
+//
5
+//  Created by Codex on 22/04/2026.
6
+//
7
+
8
+import SwiftUI
9
+
10
+struct ChargedDeviceDetailTabBarView<Tab: Hashable>: View {
11
+    let tabs: [Tab]
12
+    @Binding var selection: Tab
13
+    let tint: Color
14
+    let presentation: AdaptiveTabBarPresentation
15
+    let title: (Tab) -> String
16
+    let systemImage: (Tab) -> String
17
+
18
+    var body: some View {
19
+        HStack {
20
+            Spacer(minLength: 0)
21
+
22
+            HStack(spacing: 8) {
23
+                ForEach(tabs, id: \.self) { tab in
24
+                    let isSelected = selection == tab
25
+
26
+                    Button {
27
+                        withAnimation(.easeInOut(duration: 0.2)) {
28
+                            selection = tab
29
+                        }
30
+                    } label: {
31
+                        HStack(spacing: 6) {
32
+                            Image(systemName: systemImage(tab))
33
+                                .font(.subheadline.weight(.semibold))
34
+                            if presentation.showsTitles {
35
+                                Text(title(tab))
36
+                                    .font(.subheadline.weight(.semibold))
37
+                                    .lineLimit(1)
38
+                            }
39
+                        }
40
+                        .foregroundColor(isSelected ? .white : .primary)
41
+                        .padding(.horizontal, presentation.showsTitles ? 10 : 12)
42
+                        .padding(.vertical, presentation.showsTitles ? 7 : 10)
43
+                        .frame(maxWidth: .infinity)
44
+                        .background(
45
+                            Capsule()
46
+                                .fill(isSelected ? tint : Color.secondary.opacity(0.12))
47
+                        )
48
+                    }
49
+                    .buttonStyle(.plain)
50
+                    .accessibilityLabel(title(tab))
51
+                }
52
+            }
53
+            .frame(maxWidth: presentation.maxWidth)
54
+            .padding(6)
55
+            .background(
56
+                RoundedRectangle(cornerRadius: presentation.showsTitles ? 14 : 22, style: .continuous)
57
+                    .fill(Color.secondary.opacity(0.10))
58
+            )
59
+            .background {
60
+                RoundedRectangle(cornerRadius: presentation.showsTitles ? 14 : 22, style: .continuous)
61
+                    .fill(.ultraThinMaterial)
62
+                    .opacity(0.78)
63
+            }
64
+
65
+            Spacer(minLength: 0)
66
+        }
67
+        .padding(.horizontal, 16)
68
+        .padding(.top, 10)
69
+        .padding(.bottom, 8)
70
+        .background {
71
+            Rectangle()
72
+                .fill(.ultraThinMaterial)
73
+                .opacity(0.78)
74
+                .ignoresSafeArea(edges: .top)
75
+        }
76
+        .overlay(alignment: .bottom) {
77
+            Rectangle()
78
+                .fill(Color.secondary.opacity(0.12))
79
+                .frame(height: 1)
80
+        }
81
+    }
82
+}
+47 -0
USB Meter/Views/ChargedDevices/Components/ChargedDeviceEditorScaffoldView.swift
@@ -0,0 +1,47 @@
1
+//
2
+//  ChargedDeviceEditorScaffoldView.swift
3
+//  USB Meter
4
+//
5
+//  Created by Codex on 22/04/2026.
6
+//
7
+
8
+import SwiftUI
9
+
10
+struct ChargedDeviceEditorScaffoldView<Content: View>: View {
11
+    @Environment(\.dismiss) private var dismiss
12
+
13
+    let title: String
14
+    let saveButtonTitle: String
15
+    let canSave: Bool
16
+    let standalone: Bool
17
+    let save: () -> Void
18
+    @ViewBuilder let content: () -> Content
19
+
20
+    var body: some View {
21
+        if standalone {
22
+            NavigationView { formContent }
23
+                .navigationViewStyle(StackNavigationViewStyle())
24
+        } else {
25
+            formContent
26
+        }
27
+    }
28
+
29
+    private var formContent: some View {
30
+        Form {
31
+            content()
32
+        }
33
+        .navigationTitle(title)
34
+        .navigationBarTitleDisplayMode(.inline)
35
+        .toolbar {
36
+            ToolbarItem(placement: .cancellationAction) {
37
+                Button("Cancel") {
38
+                    dismiss()
39
+                }
40
+            }
41
+            ToolbarItem(placement: .confirmationAction) {
42
+                Button(saveButtonTitle, action: save)
43
+                    .disabled(!canSave)
44
+            }
45
+        }
46
+    }
47
+}
+83 -0
USB Meter/Views/ChargedDevices/Components/ChargedDeviceIdentityViews.swift
@@ -0,0 +1,83 @@
1
+//
2
+//  ChargedDeviceIdentityViews.swift
3
+//  USB Meter
4
+//
5
+//  Created by Codex on 22/04/2026.
6
+//
7
+
8
+import SwiftUI
9
+import UIKit
10
+
11
+struct ChargedDeviceIdentityLabelView: View {
12
+    let chargedDevice: ChargedDeviceSummary
13
+    var iconPointSize: CGFloat = 15
14
+
15
+    var body: some View {
16
+        HStack(alignment: .firstTextBaseline, spacing: 8) {
17
+            ChargedDeviceTemplateIconView(
18
+                icon: chargedDevice.identityIcon,
19
+                fallbackSystemName: chargedDevice.fallbackIdentitySymbolName,
20
+                pointSize: iconPointSize
21
+            )
22
+            Text(chargedDevice.name)
23
+        }
24
+    }
25
+}
26
+
27
+struct ChargedDeviceTemplateLabelView: View {
28
+    let template: ChargedDeviceTemplateDefinition
29
+    var iconPointSize: CGFloat = 15
30
+
31
+    var body: some View {
32
+        HStack(alignment: .firstTextBaseline, spacing: 8) {
33
+            ChargedDeviceTemplateIconView(
34
+                icon: template.icon,
35
+                fallbackSystemName: template.deviceClass.symbolName,
36
+                pointSize: iconPointSize
37
+            )
38
+            Text(template.name)
39
+        }
40
+    }
41
+}
42
+
43
+struct ChargedDeviceTemplateIconView: View {
44
+    let icon: ChargedDeviceTemplateIcon
45
+    let fallbackSystemName: String
46
+    var pointSize: CGFloat = 15
47
+
48
+    var body: some View {
49
+        Group {
50
+            if let assetName = resolvedAssetName {
51
+                Image(assetName)
52
+                    .renderingMode(.template)
53
+                    .resizable()
54
+                    .scaledToFit()
55
+            } else {
56
+                Image(systemName: resolvedSystemSymbolName)
57
+                    .font(.system(size: pointSize))
58
+            }
59
+        }
60
+        .frame(width: pointSize + 2, height: pointSize + 2)
61
+    }
62
+
63
+    private var resolvedAssetName: String? {
64
+        guard icon.type == .asset, UIImage(named: icon.name) != nil else {
65
+            return nil
66
+        }
67
+        return icon.name
68
+    }
69
+
70
+    private var resolvedSystemSymbolName: String {
71
+        let candidate = icon.resolvedSystemSymbolName(fallbackSystemName: fallbackSystemName)
72
+        if UIImage(systemName: candidate) != nil {
73
+            return candidate
74
+        }
75
+
76
+        if let fallbackSystemName = icon.fallbackSystemName,
77
+           UIImage(systemName: fallbackSystemName) != nil {
78
+            return fallbackSystemName
79
+        }
80
+
81
+        return fallbackSystemName
82
+    }
83
+}
+92 -0
USB Meter/Views/ChargedDevices/Components/ChargedDeviceLibraryRowView.swift
@@ -0,0 +1,92 @@
1
+//
2
+//  ChargedDeviceLibraryRowView.swift
3
+//  USB Meter
4
+//
5
+//  Created by Codex on 22/04/2026.
6
+//
7
+
8
+import SwiftUI
9
+
10
+struct ChargedDeviceLibraryRowView: View {
11
+    let chargedDevice: ChargedDeviceSummary
12
+    let isSelected: Bool
13
+
14
+    var body: some View {
15
+        HStack(alignment: .top, spacing: 14) {
16
+            ChargedDeviceQRCodeView(qrIdentifier: chargedDevice.qrIdentifier, side: 58)
17
+
18
+            VStack(alignment: .leading, spacing: 6) {
19
+                header
20
+                summaryText
21
+            }
22
+        }
23
+        .padding(.vertical, 4)
24
+    }
25
+
26
+    private var header: some View {
27
+        HStack {
28
+            ChargedDeviceIdentityLabelView(
29
+                chargedDevice: chargedDevice,
30
+                iconPointSize: 17
31
+            )
32
+            .font(.headline)
33
+            .foregroundColor(.primary)
34
+
35
+            Spacer()
36
+
37
+            if isSelected {
38
+                Image(systemName: "checkmark.circle.fill")
39
+                    .foregroundColor(.green)
40
+            }
41
+        }
42
+    }
43
+
44
+    @ViewBuilder
45
+    private var summaryText: some View {
46
+        Text(chargedDevice.identityTitle)
47
+            .font(.caption.weight(.semibold))
48
+            .foregroundColor(.secondary)
49
+
50
+        if chargedDevice.isCharger {
51
+            chargerSummaryText
52
+        } else {
53
+            deviceSummaryText
54
+        }
55
+    }
56
+
57
+    @ViewBuilder
58
+    private var chargerSummaryText: some View {
59
+        if !chargedDevice.chargerObservedVoltageSelections.isEmpty {
60
+            Text(chargedDevice.chargerObservedVoltageSelections.map { "\($0.format(decimalDigits: 1)) V" }.joined(separator: ", "))
61
+                .font(.caption2)
62
+                .foregroundColor(.secondary)
63
+        } else if let chargerMaximumPowerWatts = chargedDevice.chargerMaximumPowerWatts {
64
+            Text("Max power: \(chargerMaximumPowerWatts.format(decimalDigits: 2)) W")
65
+                .font(.caption2)
66
+                .foregroundColor(.secondary)
67
+        } else {
68
+            Text("Wireless charger")
69
+                .font(.caption2)
70
+                .foregroundColor(.secondary)
71
+        }
72
+    }
73
+
74
+    @ViewBuilder
75
+    private var deviceSummaryText: some View {
76
+        Text(chargedDevice.supportedChargingModes.map(\.title).joined(separator: " + "))
77
+            .font(.caption2)
78
+            .foregroundColor(.secondary)
79
+
80
+        if let capacity = chargedDevice.estimatedBatteryCapacityWh {
81
+            Text("Estimated capacity: \(capacity.format(decimalDigits: 2)) Wh")
82
+                .font(.caption)
83
+                .foregroundColor(.secondary)
84
+        }
85
+
86
+        if let minimumCurrent = chargedDevice.minimumCurrentAmps {
87
+            Text("Completion current: \(minimumCurrent.format(decimalDigits: 2)) A")
88
+                .font(.caption2)
89
+                .foregroundColor(.secondary)
90
+        }
91
+    }
92
+}
+0 -0
USB Meter/Views/ChargedDevices/ChargedDeviceQRCodeView.swift → USB Meter/Views/ChargedDevices/Components/ChargedDeviceQRCodeView.swift
File renamed without changes.
+73 -0
USB Meter/Views/ChargedDevices/Components/ChargedDeviceSidebarCardView.swift
@@ -0,0 +1,73 @@
1
+//
2
+//  ChargedDeviceSidebarCardView.swift
3
+//  USB Meter
4
+//
5
+//  Created by Codex on 22/04/2026.
6
+//
7
+
8
+import SwiftUI
9
+
10
+struct ChargedDeviceSidebarCardView: View {
11
+    let chargedDevice: ChargedDeviceSummary
12
+
13
+    var body: some View {
14
+        HStack(alignment: .top, spacing: 12) {
15
+            ChargedDeviceQRCodeView(qrIdentifier: chargedDevice.qrIdentifier, side: 54)
16
+
17
+            VStack(alignment: .leading, spacing: 6) {
18
+                header
19
+                Text(chargedDevice.identityTitle)
20
+                    .font(.caption.weight(.semibold))
21
+                    .foregroundColor(.secondary)
22
+                details
23
+            }
24
+        }
25
+        .padding(.vertical, 4)
26
+    }
27
+
28
+    private var header: some View {
29
+        HStack {
30
+            ChargedDeviceIdentityLabelView(
31
+                chargedDevice: chargedDevice,
32
+                iconPointSize: 17
33
+            )
34
+            .font(.headline)
35
+
36
+            if chargedDevice.activeSession != nil {
37
+                Spacer()
38
+                Text("Live")
39
+                    .font(.caption.weight(.bold))
40
+                    .foregroundColor(.green)
41
+            }
42
+        }
43
+    }
44
+
45
+    @ViewBuilder
46
+    private var details: some View {
47
+        if chargedDevice.isCharger {
48
+            if let chargerMaximumPowerWatts = chargedDevice.chargerMaximumPowerWatts {
49
+                Text("Max power: \(chargerMaximumPowerWatts.format(decimalDigits: 2)) W")
50
+                    .font(.caption2)
51
+                    .foregroundColor(.secondary)
52
+            } else {
53
+                Text("Wireless charger")
54
+                    .font(.caption2)
55
+                    .foregroundColor(.secondary)
56
+            }
57
+        } else {
58
+            Text(chargedDevice.supportedChargingModes.map(\.title).joined(separator: " + "))
59
+                .font(.caption2)
60
+                .foregroundColor(.secondary)
61
+
62
+            if let estimatedCapacityWh = chargedDevice.estimatedBatteryCapacityWh {
63
+                Text("Capacity: \(estimatedCapacityWh.format(decimalDigits: 2)) Wh")
64
+                    .font(.caption2)
65
+                    .foregroundColor(.secondary)
66
+            } else {
67
+                Text("Capacity: learning")
68
+                    .font(.caption2)
69
+                    .foregroundColor(.secondary)
70
+            }
71
+        }
72
+    }
73
+}
+233 -80
USB Meter/Views/ChargedDevices/ChargedDeviceDetailView.swift → USB Meter/Views/ChargedDevices/Details/ChargedDeviceDetailView.swift
@@ -8,60 +8,32 @@
8 8
 import SwiftUI
9 9
 
10 10
 struct ChargedDeviceDetailView: View {
11
+    private enum DetailTab: Hashable {
12
+        case overview
13
+        case standby
14
+        case sessions
15
+        case trends
16
+        case settings
17
+    }
18
+
11 19
     @EnvironmentObject private var appData: AppData
12 20
     @Environment(\.dismiss) private var dismiss
21
+
13 22
     @State private var editorVisibility = false
14 23
     @State private var deleteConfirmationVisibility = false
24
+    @State private var selectedTab: DetailTab = .overview
15 25
 
16 26
     let chargedDeviceID: UUID
17 27
 
18 28
     var body: some View {
19 29
         Group {
20 30
             if let chargedDevice = appData.chargedDeviceSummary(id: chargedDeviceID) {
21
-                ScrollView {
22
-                    VStack(spacing: 18) {
23
-                        headerCard(chargedDevice)
24
-                        insightsCard(chargedDevice)
25
-
26
-                        if chargedDevice.isCharger {
27
-                            standbyPowerCard(chargedDevice)
28
-                        }
29
-
30
-                        if let activeSession = chargedDevice.activeSession {
31
-                            activeSessionSummaryCard(activeSession, chargedDevice: chargedDevice)
32
-                        }
33
-
34
-                        if !chargedDevice.capacityHistory.isEmpty {
35
-                            capacityEvolutionCard(chargedDevice)
36
-                        }
37
-
38
-                        if !chargedDevice.typicalCurve.isEmpty {
39
-                            typicalCurveCard(chargedDevice)
40
-                        }
41
-
42
-                        if !closedSessions(for: chargedDevice).isEmpty {
43
-                            sessionHistorySummaryCard(chargedDevice)
44
-                        }
45
-                    }
46
-                    .padding()
47
-                }
48
-                .background(
49
-                    LinearGradient(
50
-                        colors: [tint(for: chargedDevice).opacity(0.18), Color.clear],
51
-                        startPoint: .topLeading,
52
-                        endPoint: .bottomTrailing
53
-                    )
54
-                    .ignoresSafeArea()
55
-                )
31
+                tabbedDetailView(chargedDevice)
56 32
                 .navigationTitle(chargedDevice.name)
57 33
                 .toolbar {
58 34
                     ToolbarItemGroup(placement: .primaryAction) {
59
-                        Button("Edit") {
60
-                            editorVisibility = true
61
-                        }
62
-                        Button(role: .destructive) {
63
-                            deleteConfirmationVisibility = true
64
-                        } label: {
35
+                        Button("Edit", action: showEditor)
36
+                        Button(role: .destructive, action: showDeleteConfirmation) {
65 37
                             Image(systemName: "trash")
66 38
                         }
67 39
                     }
@@ -75,10 +47,8 @@ struct ChargedDeviceDetailView: View {
75 47
         .sheet(isPresented: $editorVisibility) {
76 48
             if let chargedDevice = appData.chargedDeviceSummary(id: chargedDeviceID) {
77 49
                 if chargedDevice.isCharger {
78
-                    ChargerEditorSheetView(
79
-                        appData: appData,
80
-                        chargedDevice: chargedDevice
81
-                    )
50
+                    ChargerEditorSheetView(chargedDevice: chargedDevice)
51
+                        .environmentObject(appData)
82 52
                 } else {
83 53
                     ChargedDeviceEditorSheetView(
84 54
                         meterMACAddress: nil,
@@ -100,6 +70,116 @@ struct ChargedDeviceDetailView: View {
100 70
         }
101 71
     }
102 72
 
73
+    private func tabbedDetailView(_ chargedDevice: ChargedDeviceSummary) -> some View {
74
+        GeometryReader { proxy in
75
+            let tabs = availableTabs(for: chargedDevice)
76
+            let displayedTab = displayedTab(from: tabs)
77
+            let tabBarPresentation = AdaptiveTabBarPresentation.standard(for: proxy.size)
78
+
79
+            VStack(spacing: 0) {
80
+                ChargedDeviceDetailTabBarView(
81
+                    tabs: tabs,
82
+                    selection: $selectedTab,
83
+                    tint: tint(for: chargedDevice),
84
+                    presentation: tabBarPresentation,
85
+                    title: title(for:),
86
+                    systemImage: systemImage(for:)
87
+                )
88
+
89
+                ScrollView {
90
+                    tabContent(displayedTab, chargedDevice: chargedDevice)
91
+                        .padding()
92
+                }
93
+                .id(displayedTab)
94
+                .transition(.opacity.combined(with: .move(edge: .trailing)))
95
+                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
96
+            }
97
+            .animation(.easeInOut(duration: 0.22), value: displayedTab)
98
+            .animation(.easeInOut(duration: 0.22), value: tabs)
99
+        }
100
+        .background(detailBackground(for: chargedDevice))
101
+        .onAppear {
102
+            ensureSelectedTabExists(for: chargedDevice)
103
+        }
104
+        .onChange(of: chargedDevice.isCharger) { _ in
105
+            ensureSelectedTabExists(for: chargedDevice)
106
+        }
107
+    }
108
+
109
+    @ViewBuilder
110
+    private func tabContent(_ tab: DetailTab, chargedDevice: ChargedDeviceSummary) -> some View {
111
+        VStack(spacing: 18) {
112
+            switch tab {
113
+            case .overview:
114
+                overviewTab(chargedDevice)
115
+            case .standby:
116
+                standbyTab(chargedDevice)
117
+            case .sessions:
118
+                sessionsTab(chargedDevice)
119
+            case .trends:
120
+                trendsTab(chargedDevice)
121
+            case .settings:
122
+                settingsTab(chargedDevice)
123
+            }
124
+        }
125
+    }
126
+
127
+    @ViewBuilder
128
+    private func overviewTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
129
+        headerCard(chargedDevice)
130
+        insightsCard(chargedDevice)
131
+
132
+        if let activeSession = chargedDevice.activeSession {
133
+            activeSessionSummaryCard(activeSession, chargedDevice: chargedDevice)
134
+        }
135
+    }
136
+
137
+    @ViewBuilder
138
+    private func standbyTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
139
+        standbyPowerCard(chargedDevice)
140
+    }
141
+
142
+    @ViewBuilder
143
+    private func sessionsTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
144
+        if let activeSession = chargedDevice.activeSession {
145
+            activeSessionSummaryCard(activeSession, chargedDevice: chargedDevice)
146
+        }
147
+
148
+        if !closedSessions(for: chargedDevice).isEmpty {
149
+            sessionHistorySummaryCard(chargedDevice)
150
+        } else if chargedDevice.activeSession == nil {
151
+            emptyStateCard(
152
+                title: "No Sessions",
153
+                message: "Charging sessions will appear here after this \(chargedDevice.isCharger ? "charger" : "device") is used in a recording.",
154
+                tint: .teal
155
+            )
156
+        }
157
+    }
158
+
159
+    @ViewBuilder
160
+    private func trendsTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
161
+        if !chargedDevice.capacityHistory.isEmpty {
162
+            capacityEvolutionCard(chargedDevice)
163
+        }
164
+
165
+        if !chargedDevice.typicalCurve.isEmpty {
166
+            typicalCurveCard(chargedDevice)
167
+        }
168
+
169
+        if chargedDevice.capacityHistory.isEmpty && chargedDevice.typicalCurve.isEmpty {
170
+            emptyStateCard(
171
+                title: "Learning Trends",
172
+                message: "Capacity history and charge curves will appear after enough completed sessions are available.",
173
+                tint: .blue
174
+            )
175
+        }
176
+    }
177
+
178
+    @ViewBuilder
179
+    private func settingsTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
180
+        settingsCard(chargedDevice)
181
+    }
182
+
103 183
     private func headerCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
104 184
         HStack(alignment: .top, spacing: 18) {
105 185
             ChargedDeviceQRCodeView(qrIdentifier: chargedDevice.qrIdentifier, side: 118)
@@ -134,6 +214,58 @@ struct ChargedDeviceDetailView: View {
134 214
         .meterCard(tint: tint(for: chargedDevice), fillOpacity: 0.20, strokeOpacity: 0.26, cornerRadius: 20)
135 215
     }
136 216
 
217
+    private func settingsCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
218
+        MeterInfoCardView(title: "Settings", tint: tint(for: chargedDevice)) {
219
+            MeterInfoRowView(
220
+                label: "Kind",
221
+                value: chargedDevice.isCharger ? "Charger" : chargedDevice.deviceClass.title
222
+            )
223
+            MeterInfoRowView(label: "Template", value: chargedDevice.identityTitle)
224
+            MeterInfoRowView(label: "QR ID", value: chargedDevice.qrIdentifier)
225
+
226
+            if let meterMAC = chargedDevice.lastAssociatedMeterMAC {
227
+                MeterInfoRowView(label: "Default Meter", value: meterMAC)
228
+            }
229
+
230
+            MeterInfoRowView(label: "Created", value: chargedDevice.createdAt.format())
231
+            MeterInfoRowView(label: "Updated", value: chargedDevice.updatedAt.format())
232
+
233
+            Divider()
234
+
235
+            Button(action: showEditor) {
236
+                Label("Edit \(chargedDevice.isCharger ? "Charger" : "Device")", systemImage: "pencil")
237
+                    .font(.subheadline.weight(.semibold))
238
+                    .frame(maxWidth: .infinity)
239
+                    .padding(.vertical, 10)
240
+                    .meterCard(tint: tint(for: chargedDevice), fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
241
+            }
242
+            .buttonStyle(.plain)
243
+
244
+            Button(role: .destructive, action: showDeleteConfirmation) {
245
+                Label("Delete \(chargedDevice.isCharger ? "Charger" : "Device")", systemImage: "trash")
246
+                    .font(.subheadline.weight(.semibold))
247
+                    .frame(maxWidth: .infinity)
248
+                    .padding(.vertical, 10)
249
+                    .meterCard(tint: .red, fillOpacity: 0.10, strokeOpacity: 0.18, cornerRadius: 14)
250
+            }
251
+            .buttonStyle(.plain)
252
+        }
253
+    }
254
+
255
+    private func emptyStateCard(title: String, message: String, tint: Color) -> some View {
256
+        VStack(alignment: .leading, spacing: 8) {
257
+            Text(title)
258
+                .font(.headline)
259
+            Text(message)
260
+                .font(.footnote)
261
+                .foregroundColor(.secondary)
262
+                .fixedSize(horizontal: false, vertical: true)
263
+        }
264
+        .frame(maxWidth: .infinity, alignment: .leading)
265
+        .padding(18)
266
+        .meterCard(tint: tint, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 18)
267
+    }
268
+
137 269
     private func insightsCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
138 270
         MeterInfoCardView(title: "Insights", tint: tint(for: chargedDevice)) {
139 271
             if chargedDevice.isCharger {
@@ -514,23 +646,64 @@ struct ChargedDeviceDetailView: View {
514 646
         chargedDevice.sessions.filter { !$0.status.isOpen }
515 647
     }
516 648
 
517
-    private func wirelessSessionHint(for session: ChargeSessionSummary) -> String? {
518
-        guard session.chargingTransportMode == .wireless else {
519
-            return nil
649
+    private func availableTabs(for chargedDevice: ChargedDeviceSummary) -> [DetailTab] {
650
+        if chargedDevice.isCharger {
651
+            return [.overview, .standby, .sessions, .settings]
520 652
         }
653
+        return [.overview, .sessions, .trends, .settings]
654
+    }
521 655
 
522
-        var components: [String] = []
523
-        if let wirelessEfficiencyFactor = session.wirelessEfficiencyFactor {
524
-            components.append("Efficiency \(Int((wirelessEfficiencyFactor * 100).rounded()))%")
656
+    private func displayedTab(from tabs: [DetailTab]) -> DetailTab {
657
+        if tabs.contains(selectedTab) {
658
+            return selectedTab
525 659
         }
526
-        if session.usesEstimatedWirelessEfficiency {
527
-            components.append("Estimated from wired baseline and checkpoints")
660
+        return tabs.first ?? .overview
661
+    }
662
+
663
+    private func ensureSelectedTabExists(for chargedDevice: ChargedDeviceSummary) {
664
+        let tabs = availableTabs(for: chargedDevice)
665
+        if !tabs.contains(selectedTab) {
666
+            selectedTab = tabs.first ?? .overview
528 667
         }
529
-        if session.shouldWarnAboutLowWirelessEfficiency {
530
-            components.append("Low wireless efficiency, so capacity confidence is reduced")
668
+    }
669
+
670
+    private func title(for tab: DetailTab) -> String {
671
+        switch tab {
672
+        case .overview:
673
+            return "Overview"
674
+        case .standby:
675
+            return "Standby"
676
+        case .sessions:
677
+            return "Sessions"
678
+        case .trends:
679
+            return "Trends"
680
+        case .settings:
681
+            return "Settings"
531 682
         }
683
+    }
532 684
 
533
-        return components.isEmpty ? nil : components.joined(separator: " • ")
685
+    private func systemImage(for tab: DetailTab) -> String {
686
+        switch tab {
687
+        case .overview:
688
+            return "house.fill"
689
+        case .standby:
690
+            return "bolt.badge.clock"
691
+        case .sessions:
692
+            return "clock.arrow.trianglehead.counterclockwise.rotate.90"
693
+        case .trends:
694
+            return "chart.xyaxis.line"
695
+        case .settings:
696
+            return "gearshape.fill"
697
+        }
698
+    }
699
+
700
+    private func detailBackground(for chargedDevice: ChargedDeviceSummary) -> some View {
701
+        LinearGradient(
702
+            colors: [tint(for: chargedDevice).opacity(0.18), Color.clear],
703
+            startPoint: .topLeading,
704
+            endPoint: .bottomTrailing
705
+        )
706
+        .ignoresSafeArea()
534 707
     }
535 708
 
536 709
     private func tint(for chargedDevice: ChargedDeviceSummary) -> Color {
@@ -630,32 +803,12 @@ struct ChargedDeviceDetailView: View {
630 803
         }
631 804
     }
632 805
 
633
-    private func autoStopDescription(for session: ChargeSessionSummary) -> String {
634
-        if session.autoStopEnabled == false {
635
-            return "Manual"
636
-        }
637
-
638
-        if let sessionWarning = sessionWarning(for: session),
639
-           sessionWarning.contains("idle-current") {
640
-            return "Blocked by charger setup"
641
-        }
642
-
643
-        if session.stopThresholdAmps > 0 {
644
-            return "\(session.stopThresholdAmps.format(decimalDigits: 2)) A"
645
-        }
646
-
647
-        return "Learning"
806
+    private func showEditor() {
807
+        editorVisibility = true
648 808
     }
649 809
 
650
-    private func sessionWarning(for session: ChargeSessionSummary) -> String? {
651
-        guard session.chargingTransportMode == .wireless,
652
-              let chargerID = session.chargerID,
653
-              let charger = appData.chargedDeviceSummary(id: chargerID),
654
-              charger.chargerIdleCurrentAmps == nil else {
655
-            return nil
656
-        }
657
-
658
-        return "This charger has no idle-current measurement, so the wireless stop threshold cannot be learned or auto-applied for this session."
810
+    private func showDeleteConfirmation() {
811
+        deleteConfirmationVisibility = true
659 812
     }
660 813
 
661 814
     private var deletionTitle: String {
+0 -0
USB Meter/Views/ChargedDevices/ChargedDeviceActiveSessionView.swift → USB Meter/Views/ChargedDevices/Sessions/ChargedDeviceActiveSessionView.swift
File renamed without changes.
+0 -0
USB Meter/Views/ChargedDevices/ChargedDeviceSessionDetailView.swift → USB Meter/Views/ChargedDevices/Sessions/ChargedDeviceSessionDetailView.swift
File renamed without changes.
+0 -0
USB Meter/Views/ChargedDevices/ChargedDeviceSessionsView.swift → USB Meter/Views/ChargedDevices/Sessions/ChargedDeviceSessionsView.swift
File renamed without changes.
+0 -0
USB Meter/Views/ChargedDevices/BatteryCheckpointEditorSheetView.swift → USB Meter/Views/ChargedDevices/Sheets/ChargeSession/BatteryCheckpointEditorSheetView.swift
File renamed without changes.
+0 -0
USB Meter/Views/ChargedDevices/ChargeSessionCompletionSheetView.swift → USB Meter/Views/ChargedDevices/Sheets/ChargeSession/ChargeSessionCompletionSheetView.swift
File renamed without changes.
+15 -58
USB Meter/Views/ChargedDevices/ChargedDeviceEditorSheetView.swift → USB Meter/Views/ChargedDevices/Sheets/Editors/ChargedDeviceEditorSheetView.swift
@@ -41,11 +41,12 @@ struct ChargedDeviceEditorSheetView: View {
41 41
 
42 42
         let initialDeviceClass = chargedDevice?.deviceClass ?? .iphone
43 43
         let initialChargingStateAvailability = initialDeviceClass.normalizedChargingStateAvailability(
44
-            chargedDevice?.chargingStateAvailability ?? Self.suggestedChargingStateAvailability(for: initialDeviceClass)
44
+            chargedDevice?.chargingStateAvailability ?? initialDeviceClass.defaultChargingStateAvailability
45 45
         )
46
+        let defaultChargingSupport = initialDeviceClass.defaultChargingSupport
46 47
         let initialChargingSupport = initialDeviceClass.normalizedChargingSupport(
47
-            supportsWiredCharging: chargedDevice?.supportsWiredCharging ?? true,
48
-            supportsWirelessCharging: chargedDevice?.supportsWirelessCharging ?? true
48
+            supportsWiredCharging: chargedDevice?.supportsWiredCharging ?? defaultChargingSupport.wired,
49
+            supportsWirelessCharging: chargedDevice?.supportsWirelessCharging ?? defaultChargingSupport.wireless
49 50
         )
50 51
         let initialTemplateID = chargedDevice?.deviceTemplateID
51 52
         _deviceClass = State(initialValue: initialDeviceClass)
@@ -59,16 +60,13 @@ struct ChargedDeviceEditorSheetView: View {
59 60
     }
60 61
 
61 62
     var body: some View {
62
-        if standalone {
63
-            NavigationView { formContent }
64
-                .navigationViewStyle(StackNavigationViewStyle())
65
-        } else {
66
-            formContent
67
-        }
68
-    }
69
-
70
-    private var formContent: some View {
71
-        Form {
63
+        ChargedDeviceEditorScaffoldView(
64
+            title: editorTitle,
65
+            saveButtonTitle: saveButtonTitle,
66
+            canSave: canSave,
67
+            standalone: standalone,
68
+            save: save
69
+        ) {
72 70
             identitySection
73 71
             templateSection
74 72
             deviceChargeBehaviourSection
@@ -76,21 +74,6 @@ struct ChargedDeviceEditorSheetView: View {
76 74
             deviceCompletionSection
77 75
             notesSection
78 76
         }
79
-        .navigationTitle(editorTitle)
80
-        .navigationBarTitleDisplayMode(.inline)
81
-        .toolbar {
82
-            ToolbarItem(placement: .cancellationAction) {
83
-                Button("Cancel") {
84
-                    dismiss()
85
-                }
86
-            }
87
-            ToolbarItem(placement: .confirmationAction) {
88
-                Button(saveButtonTitle) {
89
-                    save()
90
-                }
91
-                .disabled(!canSave)
92
-            }
93
-        }
94 77
         .onChange(of: deviceClass) { newValue in
95 78
             applyDeviceClassRules(for: newValue)
96 79
         }
@@ -437,30 +420,16 @@ struct ChargedDeviceEditorSheetView: View {
437 420
         if let enforcedChargingStateAvailability = deviceClass.enforcedChargingStateAvailability {
438 421
             chargingStateAvailability = enforcedChargingStateAvailability
439 422
         } else if chargedDevice == nil {
440
-            chargingStateAvailability = Self.suggestedChargingStateAvailability(for: deviceClass)
423
+            chargingStateAvailability = deviceClass.defaultChargingStateAvailability
441 424
         }
442 425
 
443 426
         if let enforcedChargingSupport = deviceClass.enforcedChargingSupport {
444 427
             supportsWiredCharging = enforcedChargingSupport.wired
445 428
             supportsWirelessCharging = enforcedChargingSupport.wireless
446 429
         } else if chargedDevice == nil {
447
-            switch deviceClass {
448
-            case .iphone:
449
-                supportsWiredCharging = true
450
-                supportsWirelessCharging = true
451
-            case .watch:
452
-                supportsWiredCharging = false
453
-                supportsWirelessCharging = true
454
-            case .powerbank:
455
-                supportsWiredCharging = true
456
-                supportsWirelessCharging = false
457
-            case .charger:
458
-                supportsWiredCharging = false
459
-                supportsWirelessCharging = true
460
-            case .other:
461
-                supportsWiredCharging = true
462
-                supportsWirelessCharging = false
463
-            }
430
+            let defaultChargingSupport = deviceClass.defaultChargingSupport
431
+            supportsWiredCharging = defaultChargingSupport.wired
432
+            supportsWirelessCharging = defaultChargingSupport.wireless
464 433
         }
465 434
     }
466 435
 
@@ -528,16 +497,4 @@ struct ChargedDeviceEditorSheetView: View {
528 497
         }
529 498
     }
530 499
 
531
-    private static func suggestedChargingStateAvailability(for deviceClass: ChargedDeviceClass) -> ChargingStateAvailability {
532
-        switch deviceClass {
533
-        case .iphone:
534
-            return .onOrOff
535
-        case .watch:
536
-            return .onOnly
537
-        case .powerbank:
538
-            return .offOnly
539
-        case .charger, .other:
540
-            return .onOrOff
541
-        }
542
-    }
543 500
 }
+20 -26
USB Meter/Views/ChargedDevices/ChargerEditorSheetView.swift → USB Meter/Views/ChargedDevices/Sheets/Editors/ChargerEditorSheetView.swift
@@ -6,9 +6,9 @@
6 6
 import SwiftUI
7 7
 
8 8
 struct ChargerEditorSheetView: View {
9
+    @EnvironmentObject private var appData: AppData
9 10
     @Environment(\.dismiss) private var dismiss
10 11
 
11
-    let appData: AppData
12 12
     let chargedDevice: ChargedDeviceSummary?
13 13
     let meterMACAddress: String?
14 14
     /// When false the view omits its own NavigationView (used as a push destination).
@@ -19,12 +19,10 @@ struct ChargerEditorSheetView: View {
19 19
     @State private var notes: String
20 20
 
21 21
     init(
22
-        appData: AppData,
23 22
         chargedDevice: ChargedDeviceSummary? = nil,
24 23
         meterMACAddress: String? = nil,
25 24
         standalone: Bool = true
26 25
     ) {
27
-        self.appData = appData
28 26
         self.chargedDevice = chargedDevice
29 27
         self.meterMACAddress = meterMACAddress
30 28
         self.standalone = standalone
@@ -34,16 +32,13 @@ struct ChargerEditorSheetView: View {
34 32
     }
35 33
 
36 34
     var body: some View {
37
-        if standalone {
38
-            NavigationView { formContent }
39
-                .navigationViewStyle(StackNavigationViewStyle())
40
-        } else {
41
-            formContent
42
-        }
43
-    }
44
-
45
-    private var formContent: some View {
46
-        Form {
35
+        ChargedDeviceEditorScaffoldView(
36
+            title: editorTitle,
37
+            saveButtonTitle: saveButtonTitle,
38
+            canSave: canSave,
39
+            standalone: standalone,
40
+            save: save
41
+        ) {
47 42
             Section(header: Text("Identity")) {
48 43
                 TextField("Charger name", text: $name)
49 44
 
@@ -74,19 +69,18 @@ struct ChargerEditorSheetView: View {
74 69
                 TextField("Optional notes", text: $notes)
75 70
             }
76 71
         }
77
-        .navigationTitle(chargedDevice == nil ? "New Charger" : "Edit Charger")
78
-        .navigationBarTitleDisplayMode(.inline)
79
-        .toolbar {
80
-            ToolbarItem(placement: .cancellationAction) {
81
-                Button("Cancel") { dismiss() }
82
-            }
83
-            ToolbarItem(placement: .confirmationAction) {
84
-                Button(chargedDevice == nil ? "Save" : "Update") {
85
-                    save()
86
-                }
87
-                .disabled(name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
88
-            }
89
-        }
72
+    }
73
+
74
+    private var editorTitle: String {
75
+        chargedDevice == nil ? "New Charger" : "Edit Charger"
76
+    }
77
+
78
+    private var saveButtonTitle: String {
79
+        chargedDevice == nil ? "Save" : "Update"
80
+    }
81
+
82
+    private var canSave: Bool {
83
+        !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
90 84
     }
91 85
 
92 86
     private func save() {
+229 -0
USB Meter/Views/ChargedDevices/Sheets/Library/ChargedDeviceLibrarySheetView.swift
@@ -0,0 +1,229 @@
1
+//
2
+//  ChargedDeviceLibrarySheetView.swift
3
+//  USB Meter
4
+//
5
+//  Created by Codex on 10/04/2026.
6
+//
7
+
8
+import SwiftUI
9
+
10
+enum ChargedDeviceLibraryMode {
11
+    case device
12
+    case charger
13
+
14
+    var kind: ChargedDeviceKind {
15
+        switch self {
16
+        case .device:
17
+            return .device
18
+        case .charger:
19
+            return .charger
20
+        }
21
+    }
22
+
23
+    var title: String {
24
+        kind.pluralTitle
25
+    }
26
+
27
+    var singularTitle: String {
28
+        kind.title
29
+    }
30
+}
31
+
32
+struct ChargedDeviceLibrarySheetView: View {
33
+    @EnvironmentObject private var appData: AppData
34
+    @Environment(\.dismiss) private var dismiss
35
+
36
+    let meterMACAddress: String
37
+    let meterTint: Color
38
+    let mode: ChargedDeviceLibraryMode
39
+    /// true = standalone sheet with own NavigationView; false = pushed into parent nav stack
40
+    let standalone: Bool
41
+
42
+    @State private var showingNewEditor = false
43
+    @State private var editingChargedDevice: ChargedDeviceSummary?
44
+    @State private var pendingDeletion: ChargedDeviceSummary?
45
+
46
+    init(
47
+        meterMACAddress: String,
48
+        meterTint: Color,
49
+        mode: ChargedDeviceLibraryMode,
50
+        standalone: Bool = true
51
+    ) {
52
+        self.meterMACAddress = meterMACAddress
53
+        self.meterTint = meterTint
54
+        self.mode = mode
55
+        self.standalone = standalone
56
+    }
57
+
58
+    var body: some View {
59
+        if standalone {
60
+            NavigationView { listContent }
61
+                .navigationViewStyle(StackNavigationViewStyle())
62
+        } else {
63
+            listContent
64
+        }
65
+    }
66
+
67
+    private var listContent: some View {
68
+        List {
69
+            if displayedChargedDevices.isEmpty {
70
+                VStack(alignment: .leading, spacing: 10) {
71
+                    HStack(spacing: 8) {
72
+                        Text("No \(mode.title.lowercased()) yet.")
73
+                            .font(.headline)
74
+                        ContextInfoButton(
75
+                            title: mode.title,
76
+                            message: emptyStateDescription
77
+                        )
78
+                    }
79
+                }
80
+                .padding(.vertical, 10)
81
+                .listRowBackground(Color.clear)
82
+            } else {
83
+                ForEach(displayedChargedDevices) { chargedDevice in
84
+                    Button {
85
+                        select(chargedDevice)
86
+                        dismiss()
87
+                    } label: {
88
+                        ChargedDeviceLibraryRowView(
89
+                            chargedDevice: chargedDevice,
90
+                            isSelected: chargedDevice.id == selectedDeviceID
91
+                        )
92
+                    }
93
+                    .buttonStyle(.plain)
94
+                    .swipeActions(edge: .trailing, allowsFullSwipe: false) {
95
+                        Button(role: .destructive) {
96
+                            pendingDeletion = chargedDevice
97
+                        } label: {
98
+                            Label("Delete", systemImage: "trash")
99
+                        }
100
+                        Button {
101
+                            editingChargedDevice = chargedDevice
102
+                        } label: {
103
+                            Label("Edit", systemImage: "pencil")
104
+                        }
105
+                        .tint(.blue)
106
+                    }
107
+                    .contextMenu {
108
+                        Button {
109
+                            editingChargedDevice = chargedDevice
110
+                        } label: {
111
+                            Label("Edit \(mode.singularTitle)", systemImage: "pencil")
112
+                        }
113
+                        Button(role: .destructive) {
114
+                            pendingDeletion = chargedDevice
115
+                        } label: {
116
+                            Label("Delete \(mode.singularTitle)", systemImage: "trash")
117
+                        }
118
+                    }
119
+                }
120
+            }
121
+        }
122
+        .listStyle(InsetGroupedListStyle())
123
+        .background(
124
+            LinearGradient(
125
+                colors: [meterTint.opacity(0.14), Color.clear],
126
+                startPoint: .topLeading,
127
+                endPoint: .bottomTrailing
128
+            )
129
+            .ignoresSafeArea()
130
+        )
131
+        .navigationTitle(mode.title)
132
+        .navigationBarTitleDisplayMode(.inline)
133
+        .toolbar {
134
+            ToolbarItem(placement: .cancellationAction) {
135
+                if standalone {
136
+                    Button("Done") { dismiss() }
137
+                }
138
+            }
139
+            ToolbarItem(placement: .confirmationAction) {
140
+                Button("New") { showingNewEditor = true }
141
+            }
142
+        }
143
+        .sheet(isPresented: $showingNewEditor) {
144
+            newEditorSheet
145
+        }
146
+        .sheet(item: $editingChargedDevice) { device in
147
+            editEditorSheet(device)
148
+        }
149
+        .confirmationDialog(
150
+            "Delete \(pendingDeletion?.name ?? mode.singularTitle)?",
151
+            isPresented: Binding(
152
+                get: { pendingDeletion != nil },
153
+                set: { if !$0 { pendingDeletion = nil } }
154
+            ),
155
+            titleVisibility: .visible
156
+        ) {
157
+            Button("Delete", role: .destructive) {
158
+                if let device = pendingDeletion {
159
+                    _ = appData.deleteChargedDevice(id: device.id)
160
+                    pendingDeletion = nil
161
+                }
162
+            }
163
+            Button("Cancel", role: .cancel) { pendingDeletion = nil }
164
+        } message: {
165
+            Text("This will permanently remove the \(mode.singularTitle.lowercased()) and all associated data.")
166
+        }
167
+    }
168
+
169
+    @ViewBuilder
170
+    private var newEditorSheet: some View {
171
+        if mode == .charger {
172
+            ChargerEditorSheetView(meterMACAddress: meterMACAddress)
173
+                .environmentObject(appData)
174
+        } else {
175
+            ChargedDeviceEditorSheetView(meterMACAddress: meterMACAddress)
176
+                .environmentObject(appData)
177
+        }
178
+    }
179
+
180
+    @ViewBuilder
181
+    private func editEditorSheet(_ chargedDevice: ChargedDeviceSummary) -> some View {
182
+        if chargedDevice.isCharger {
183
+            ChargerEditorSheetView(chargedDevice: chargedDevice)
184
+                .environmentObject(appData)
185
+        } else {
186
+            ChargedDeviceEditorSheetView(
187
+                meterMACAddress: nil,
188
+                chargedDevice: chargedDevice
189
+            )
190
+            .environmentObject(appData)
191
+        }
192
+    }
193
+
194
+    private var displayedChargedDevices: [ChargedDeviceSummary] {
195
+        switch mode {
196
+        case .device:
197
+            return appData.deviceSummaries
198
+        case .charger:
199
+            return appData.chargerSummaries
200
+        }
201
+    }
202
+
203
+    private var selectedDeviceID: UUID? {
204
+        switch mode {
205
+        case .device:
206
+            return appData.currentChargedDeviceSummary(for: meterMACAddress)?.id
207
+        case .charger:
208
+            return appData.currentChargerSummary(for: meterMACAddress)?.id
209
+        }
210
+    }
211
+
212
+    private var emptyStateDescription: String {
213
+        switch mode {
214
+        case .device:
215
+            return "Create one here, then select it before or during a charging session. The selected device becomes the default for this meter."
216
+        case .charger:
217
+            return "Create one here, then select it for wireless charging sessions. The selected charger becomes the default wireless source for this meter."
218
+        }
219
+    }
220
+
221
+    private func select(_ chargedDevice: ChargedDeviceSummary) {
222
+        switch mode {
223
+        case .device:
224
+            appData.assignChargedDevice(chargedDevice.id, to: meterMACAddress)
225
+        case .charger:
226
+            appData.assignCharger(chargedDevice.id, to: meterMACAddress)
227
+        }
228
+    }
229
+}
+158 -0
USB Meter/Views/ChargedDevices/Sidebar/SidebarChargedDeviceLibraryView.swift
@@ -0,0 +1,158 @@
1
+//
2
+//  SidebarChargedDeviceLibraryView.swift
3
+//  USB Meter
4
+//
5
+//  Created by Codex on 22/04/2026.
6
+//
7
+
8
+import SwiftUI
9
+
10
+/// Full-management library for the sidebar — navigates into detail instead of select-and-dismiss.
11
+struct SidebarChargedDeviceLibraryView: View {
12
+    @EnvironmentObject private var appData: AppData
13
+
14
+    let mode: ChargedDeviceLibraryMode
15
+
16
+    @State private var showingNewEditor = false
17
+    @State private var editingChargedDevice: ChargedDeviceSummary?
18
+    @State private var pendingDeletion: ChargedDeviceSummary?
19
+
20
+    private var tint: Color {
21
+        mode == .device ? .orange : .pink
22
+    }
23
+
24
+    var body: some View {
25
+        List {
26
+            if displayedDevices.isEmpty {
27
+                emptyStateView
28
+            } else {
29
+                deviceRows
30
+            }
31
+        }
32
+        .listStyle(InsetGroupedListStyle())
33
+        .background(backgroundGradient)
34
+        .navigationTitle(mode.title)
35
+        .navigationBarTitleDisplayMode(.inline)
36
+        .toolbar {
37
+            ToolbarItem(placement: .primaryAction) {
38
+                Button("New") { showingNewEditor = true }
39
+            }
40
+        }
41
+        .sheet(isPresented: $showingNewEditor) { newEditorSheet }
42
+        .sheet(item: $editingChargedDevice) { device in editEditorSheet(device) }
43
+        .confirmationDialog(
44
+            "Delete \(pendingDeletion?.name ?? mode.singularTitle)?",
45
+            isPresented: Binding(
46
+                get: { pendingDeletion != nil },
47
+                set: { if !$0 { pendingDeletion = nil } }
48
+            ),
49
+            titleVisibility: .visible
50
+        ) {
51
+            Button("Delete", role: .destructive, action: deletePendingDevice)
52
+            Button("Cancel", role: .cancel) { pendingDeletion = nil }
53
+        } message: {
54
+            Text("This will permanently remove the \(mode.singularTitle.lowercased()) and all associated data.")
55
+        }
56
+    }
57
+
58
+    private var emptyStateView: some View {
59
+        VStack(alignment: .leading, spacing: 10) {
60
+            HStack(spacing: 8) {
61
+                Text("No \(mode.title.lowercased()) yet.")
62
+                    .font(.headline)
63
+                ContextInfoButton(
64
+                    title: mode.title,
65
+                    message: emptyStateDescription
66
+                )
67
+            }
68
+        }
69
+        .padding(.vertical, 10)
70
+        .listRowBackground(Color.clear)
71
+    }
72
+
73
+    private var deviceRows: some View {
74
+        ForEach(displayedDevices) { device in
75
+            NavigationLink(destination: ChargedDeviceDetailView(chargedDeviceID: device.id)) {
76
+                ChargedDeviceLibraryRowView(chargedDevice: device, isSelected: false)
77
+            }
78
+            .swipeActions(edge: .trailing, allowsFullSwipe: false) {
79
+                rowActions(for: device)
80
+            }
81
+            .contextMenu {
82
+                Button {
83
+                    editingChargedDevice = device
84
+                } label: {
85
+                    Label("Edit \(mode.singularTitle)", systemImage: "pencil")
86
+                }
87
+                Button(role: .destructive) {
88
+                    pendingDeletion = device
89
+                } label: {
90
+                    Label("Delete \(mode.singularTitle)", systemImage: "trash")
91
+                }
92
+            }
93
+        }
94
+    }
95
+
96
+    private var backgroundGradient: some View {
97
+        LinearGradient(
98
+            colors: [tint.opacity(0.14), Color.clear],
99
+            startPoint: .topLeading,
100
+            endPoint: .bottomTrailing
101
+        )
102
+        .ignoresSafeArea()
103
+    }
104
+
105
+    private var displayedDevices: [ChargedDeviceSummary] {
106
+        mode == .device ? appData.deviceSummaries : appData.chargerSummaries
107
+    }
108
+
109
+    @ViewBuilder
110
+    private var newEditorSheet: some View {
111
+        if mode == .charger {
112
+            ChargerEditorSheetView(meterMACAddress: nil)
113
+                .environmentObject(appData)
114
+        } else {
115
+            ChargedDeviceEditorSheetView(meterMACAddress: nil)
116
+                .environmentObject(appData)
117
+        }
118
+    }
119
+
120
+    @ViewBuilder
121
+    private func editEditorSheet(_ device: ChargedDeviceSummary) -> some View {
122
+        if device.isCharger {
123
+            ChargerEditorSheetView(chargedDevice: device)
124
+                .environmentObject(appData)
125
+        } else {
126
+            ChargedDeviceEditorSheetView(meterMACAddress: nil, chargedDevice: device)
127
+                .environmentObject(appData)
128
+        }
129
+    }
130
+
131
+    @ViewBuilder
132
+    private func rowActions(for device: ChargedDeviceSummary) -> some View {
133
+        Button(role: .destructive) {
134
+            pendingDeletion = device
135
+        } label: {
136
+            Label("Delete", systemImage: "trash")
137
+        }
138
+        Button {
139
+            editingChargedDevice = device
140
+        } label: {
141
+            Label("Edit", systemImage: "pencil")
142
+        }
143
+        .tint(.blue)
144
+    }
145
+
146
+    private var emptyStateDescription: String {
147
+        mode == .device
148
+            ? "Create one here, then select it before or during a charging session. The selected device becomes the default for this meter."
149
+            : "Create one here, then select it for wireless charging sessions. The selected charger becomes the default wireless source for this meter."
150
+    }
151
+
152
+    private func deletePendingDevice() {
153
+        if let device = pendingDeletion {
154
+            _ = appData.deleteChargedDevice(id: device.id)
155
+            pendingDeletion = nil
156
+        }
157
+    }
158
+}
+0 -57
USB Meter/Views/ChargedDevices/SidebarChargedDevicesSectionView.swift → USB Meter/Views/ChargedDevices/Sidebar/SidebarChargedDevicesSectionView.swift
@@ -91,60 +91,3 @@ struct SidebarChargedDevicesSectionView: View {
91 91
         }
92 92
     }
93 93
 }
94
-
95
-private struct ChargedDeviceSidebarCardView: View {
96
-    let chargedDevice: ChargedDeviceSummary
97
-
98
-    var body: some View {
99
-        HStack(alignment: .top, spacing: 12) {
100
-            ChargedDeviceQRCodeView(qrIdentifier: chargedDevice.qrIdentifier, side: 54)
101
-
102
-            VStack(alignment: .leading, spacing: 6) {
103
-                HStack {
104
-                    ChargedDeviceIdentityLabelView(
105
-                        chargedDevice: chargedDevice,
106
-                        iconPointSize: 17
107
-                    )
108
-                    .font(.headline)
109
-                    if chargedDevice.activeSession != nil {
110
-                        Spacer()
111
-                        Text("Live")
112
-                            .font(.caption.weight(.bold))
113
-                            .foregroundColor(.green)
114
-                    }
115
-                }
116
-
117
-                Text(chargedDevice.identityTitle)
118
-                    .font(.caption.weight(.semibold))
119
-                    .foregroundColor(.secondary)
120
-
121
-                if chargedDevice.isCharger {
122
-                    if let chargerMaximumPowerWatts = chargedDevice.chargerMaximumPowerWatts {
123
-                        Text("Max power: \(chargerMaximumPowerWatts.format(decimalDigits: 2)) W")
124
-                            .font(.caption2)
125
-                            .foregroundColor(.secondary)
126
-                    } else {
127
-                        Text("Wireless charger")
128
-                            .font(.caption2)
129
-                            .foregroundColor(.secondary)
130
-                    }
131
-                } else {
132
-                    Text(chargedDevice.supportedChargingModes.map(\.title).joined(separator: " + "))
133
-                        .font(.caption2)
134
-                        .foregroundColor(.secondary)
135
-
136
-                    if let estimatedCapacityWh = chargedDevice.estimatedBatteryCapacityWh {
137
-                        Text("Capacity: \(estimatedCapacityWh.format(decimalDigits: 2)) Wh")
138
-                            .font(.caption2)
139
-                            .foregroundColor(.secondary)
140
-                    } else {
141
-                        Text("Capacity: learning")
142
-                            .font(.caption2)
143
-                            .foregroundColor(.secondary)
144
-                    }
145
-                }
146
-            }
147
-        }
148
-        .padding(.vertical, 4)
149
-    }
150
-}
+29 -0
USB Meter/Views/Components/Generic/AdaptiveTabBarPresentation.swift
@@ -0,0 +1,29 @@
1
+//
2
+//  AdaptiveTabBarPresentation.swift
3
+//  USB Meter
4
+//
5
+//  Created by Codex on 22/04/2026.
6
+//
7
+
8
+import SwiftUI
9
+
10
+struct AdaptiveTabBarPresentation: Equatable {
11
+    let showsTitles: Bool
12
+    let maxWidth: CGFloat
13
+
14
+    static func standard(for size: CGSize) -> AdaptiveTabBarPresentation {
15
+        let compact = min(size.width, size.height)
16
+
17
+        if compact < 700 {
18
+            return AdaptiveTabBarPresentation(
19
+                showsTitles: false,
20
+                maxWidth: 340
21
+            )
22
+        }
23
+
24
+        return AdaptiveTabBarPresentation(
25
+            showsTitles: true,
26
+            maxWidth: min(size.width - 32, 680)
27
+        )
28
+    }
29
+}
+85 -27
USB Meter/Views/Meter/MeterView.swift
@@ -12,28 +12,24 @@ import CoreBluetooth
12 12
 
13 13
 struct MeterView: View {
14 14
     private struct TabBarStyle {
15
-        let showsTitles: Bool
16 15
         let horizontalPadding: CGFloat
17 16
         let topPadding: CGFloat
18 17
         let bottomPadding: CGFloat
19 18
         let chipHorizontalPadding: CGFloat
20 19
         let chipVerticalPadding: CGFloat
21 20
         let outerPadding: CGFloat
22
-        let maxWidth: CGFloat
23 21
         let barBackgroundOpacity: CGFloat
24 22
         let materialOpacity: CGFloat
25 23
         let shadowOpacity: CGFloat
26 24
         let floatingInset: CGFloat
27 25
 
28 26
         static let portrait = TabBarStyle(
29
-            showsTitles: true,
30 27
             horizontalPadding: 16,
31 28
             topPadding: 10,
32 29
             bottomPadding: 8,
33 30
             chipHorizontalPadding: 10,
34 31
             chipVerticalPadding: 7,
35 32
             outerPadding: 6,
36
-            maxWidth: 420,
37 33
             barBackgroundOpacity: 0.10,
38 34
             materialOpacity: 0.78,
39 35
             shadowOpacity: 0,
@@ -41,14 +37,12 @@ struct MeterView: View {
41 37
         )
42 38
 
43 39
         static let portraitCompact = TabBarStyle(
44
-            showsTitles: false,
45 40
             horizontalPadding: 16,
46 41
             topPadding: 10,
47 42
             bottomPadding: 8,
48 43
             chipHorizontalPadding: 12,
49 44
             chipVerticalPadding: 10,
50 45
             outerPadding: 6,
51
-            maxWidth: 320,
52 46
             barBackgroundOpacity: 0.14,
53 47
             materialOpacity: 0.90,
54 48
             shadowOpacity: 0,
@@ -56,14 +50,12 @@ struct MeterView: View {
56 50
         )
57 51
 
58 52
         static let landscapeInline = TabBarStyle(
59
-            showsTitles: true,
60 53
             horizontalPadding: 12,
61 54
             topPadding: 10,
62 55
             bottomPadding: 8,
63 56
             chipHorizontalPadding: 10,
64 57
             chipVerticalPadding: 7,
65 58
             outerPadding: 6,
66
-            maxWidth: 420,
67 59
             barBackgroundOpacity: 0.10,
68 60
             materialOpacity: 0.78,
69 61
             shadowOpacity: 0,
@@ -71,14 +63,12 @@ struct MeterView: View {
71 63
         )
72 64
 
73 65
         static let landscapeFloating = TabBarStyle(
74
-            showsTitles: false,
75 66
             horizontalPadding: 16,
76 67
             topPadding: 10,
77 68
             bottomPadding: 0,
78 69
             chipHorizontalPadding: 11,
79 70
             chipVerticalPadding: 11,
80 71
             outerPadding: 7,
81
-            maxWidth: 260,
82 72
             barBackgroundOpacity: 0.16,
83 73
             materialOpacity: 0.88,
84 74
             shadowOpacity: 0.12,
@@ -131,6 +121,10 @@ struct MeterView: View {
131 121
                 usesOverlayTabBar: usesOverlayTabBar,
132 122
                 size: proxy.size
133 123
             )
124
+            let tabBarPresentation = tabBarPresentation(
125
+                for: proxy.size,
126
+                usesOverlayTabBar: usesOverlayTabBar
127
+            )
134 128
 
135 129
             VStack(spacing: 0) {
136 130
                 if Self.isMacIPadApp {
@@ -141,10 +135,15 @@ struct MeterView: View {
141 135
                         landscapeDeck(
142 136
                             size: proxy.size,
143 137
                             usesOverlayTabBar: usesOverlayTabBar,
144
-                            tabBarStyle: tabBarStyle
138
+                            tabBarStyle: tabBarStyle,
139
+                            tabBarPresentation: tabBarPresentation
145 140
                         )
146 141
                     } else {
147
-                        portraitContent(size: proxy.size, tabBarStyle: tabBarStyle)
142
+                        portraitContent(
143
+                            size: proxy.size,
144
+                            tabBarStyle: tabBarStyle,
145
+                            tabBarPresentation: tabBarPresentation
146
+                        )
148 147
                     }
149 148
                 }
150 149
                 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
@@ -231,20 +230,45 @@ struct MeterView: View {
231 230
         }
232 231
     }
233 232
 
234
-    private func portraitContent(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
235
-        portraitSegmentedDeck(size: size, tabBarStyle: tabBarStyle)
233
+    private func portraitContent(
234
+        size: CGSize,
235
+        tabBarStyle: TabBarStyle,
236
+        tabBarPresentation: AdaptiveTabBarPresentation
237
+    ) -> some View {
238
+        portraitSegmentedDeck(
239
+            size: size,
240
+            tabBarStyle: tabBarStyle,
241
+            tabBarPresentation: tabBarPresentation
242
+        )
236 243
     }
237 244
 
238 245
     @ViewBuilder
239
-    private func landscapeDeck(size: CGSize, usesOverlayTabBar: Bool, tabBarStyle: TabBarStyle) -> some View {
246
+    private func landscapeDeck(
247
+        size: CGSize,
248
+        usesOverlayTabBar: Bool,
249
+        tabBarStyle: TabBarStyle,
250
+        tabBarPresentation: AdaptiveTabBarPresentation
251
+    ) -> some View {
240 252
         if usesOverlayTabBar {
241
-            landscapeOverlaySegmentedDeck(size: size, tabBarStyle: tabBarStyle)
253
+            landscapeOverlaySegmentedDeck(
254
+                size: size,
255
+                tabBarStyle: tabBarStyle,
256
+                tabBarPresentation: tabBarPresentation
257
+            )
242 258
         } else {
243
-            landscapeSegmentedDeck(size: size, tabBarStyle: tabBarStyle)
259
+            landscapeSegmentedDeck(
260
+                size: size,
261
+                tabBarStyle: tabBarStyle,
262
+                tabBarPresentation: tabBarPresentation
263
+            )
244 264
         }
245 265
     }
246 266
 
247
-    private func landscapeOverlaySegmentedDeck(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
267
+    private func landscapeOverlaySegmentedDeck(
268
+        size: CGSize,
269
+        tabBarStyle: TabBarStyle,
270
+        tabBarPresentation: AdaptiveTabBarPresentation
271
+    ) -> some View {
248 272
         ZStack(alignment: .top) {
249 273
             landscapeSegmentedContent(size: size)
250 274
                 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
@@ -252,7 +276,11 @@ struct MeterView: View {
252 276
                 .id(displayedMeterTab)
253 277
                 .transition(.opacity.combined(with: .move(edge: .trailing)))
254 278
 
255
-            segmentedTabBar(style: tabBarStyle, showsConnectionAction: !Self.isMacIPadApp)
279
+            segmentedTabBar(
280
+                style: tabBarStyle,
281
+                presentation: tabBarPresentation,
282
+                showsConnectionAction: !Self.isMacIPadApp
283
+            )
256 284
         }
257 285
         .animation(.easeInOut(duration: 0.22), value: displayedMeterTab)
258 286
         .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
@@ -266,9 +294,17 @@ struct MeterView: View {
266 294
         }
267 295
     }
268 296
 
269
-    private func landscapeSegmentedDeck(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
297
+    private func landscapeSegmentedDeck(
298
+        size: CGSize,
299
+        tabBarStyle: TabBarStyle,
300
+        tabBarPresentation: AdaptiveTabBarPresentation
301
+    ) -> some View {
270 302
         VStack(spacing: 0) {
271
-            segmentedTabBar(style: tabBarStyle, showsConnectionAction: !Self.isMacIPadApp)
303
+            segmentedTabBar(
304
+                style: tabBarStyle,
305
+                presentation: tabBarPresentation,
306
+                showsConnectionAction: !Self.isMacIPadApp
307
+            )
272 308
 
273 309
             landscapeSegmentedContent(size: size)
274 310
                 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
@@ -282,9 +318,16 @@ struct MeterView: View {
282 318
         }
283 319
     }
284 320
 
285
-    private func portraitSegmentedDeck(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
321
+    private func portraitSegmentedDeck(
322
+        size: CGSize,
323
+        tabBarStyle: TabBarStyle,
324
+        tabBarPresentation: AdaptiveTabBarPresentation
325
+    ) -> some View {
286 326
         VStack(spacing: 0) {
287
-            segmentedTabBar(style: tabBarStyle)
327
+            segmentedTabBar(
328
+                style: tabBarStyle,
329
+                presentation: tabBarPresentation
330
+            )
288 331
 
289 332
             portraitSegmentedContent(size: size)
290 333
                 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
@@ -298,9 +341,13 @@ struct MeterView: View {
298 341
         }
299 342
     }
300 343
 
301
-    private func segmentedTabBar(style: TabBarStyle, showsConnectionAction: Bool = false) -> some View {
344
+    private func segmentedTabBar(
345
+        style: TabBarStyle,
346
+        presentation: AdaptiveTabBarPresentation,
347
+        showsConnectionAction: Bool = false
348
+    ) -> some View {
302 349
         let isFloating = style.floatingInset > 0
303
-        let cornerRadius = style.showsTitles ? 14.0 : 22.0
350
+        let cornerRadius = presentation.showsTitles ? 14.0 : 22.0
304 351
         let unselectedForegroundColor = isFloating ? Color.primary.opacity(0.86) : Color.primary
305 352
         let unselectedChipFill = isFloating ? Color.black.opacity(0.08) : Color.secondary.opacity(0.12)
306 353
 
@@ -319,7 +366,7 @@ struct MeterView: View {
319 366
                         HStack(spacing: 6) {
320 367
                             Image(systemName: tab.systemImage)
321 368
                                 .font(.subheadline.weight(.semibold))
322
-                            if style.showsTitles {
369
+                            if presentation.showsTitles {
323 370
                                 Text(title(for: tab))
324 371
                                     .font(.subheadline.weight(.semibold))
325 372
                                     .lineLimit(1)
@@ -346,7 +393,7 @@ struct MeterView: View {
346 393
                     .accessibilityLabel(title(for: tab))
347 394
                 }
348 395
             }
349
-            .frame(maxWidth: style.maxWidth)
396
+            .frame(maxWidth: presentation.maxWidth)
350 397
             .padding(style.outerPadding)
351 398
             .background(
352 399
                 RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
@@ -557,6 +604,17 @@ struct MeterView: View {
557 604
         return .portrait
558 605
     }
559 606
 
607
+    private func tabBarPresentation(for size: CGSize, usesOverlayTabBar: Bool) -> AdaptiveTabBarPresentation {
608
+        if usesOverlayTabBar {
609
+            return AdaptiveTabBarPresentation(
610
+                showsTitles: false,
611
+                maxWidth: 260
612
+            )
613
+        }
614
+
615
+        return AdaptiveTabBarPresentation.standard(for: size)
616
+    }
617
+
560 618
     private func landscapeContentTopPadding(for style: TabBarStyle) -> CGFloat {
561 619
         if style.floatingInset > 0 {
562 620
             return max(landscapeTabBarHeight * 0.44, 26)
+2 -1
USB Meter/Views/Sidebar/SidebarView.swift
@@ -66,7 +66,8 @@ struct SidebarView: View {
66 66
                 )
67 67
                 .environmentObject(appData)
68 68
             case .charger:
69
-                ChargerEditorSheetView(appData: appData)
69
+                ChargerEditorSheetView()
70
+                    .environmentObject(appData)
70 71
             }
71 72
         }
72 73
     }