Showing 3 changed files with 171 additions and 13 deletions
+134 -0
USB Meter/Views/ChargedDevices/ChargedDeviceLibrarySheetView.swift
@@ -384,3 +384,137 @@ struct ChargedDeviceTemplateIconView: View {
384 384
         return fallbackSystemName
385 385
     }
386 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
+}
+35 -13
USB Meter/Views/ChargedDevices/SidebarChargedDevicesSectionView.swift
@@ -9,6 +9,7 @@ import SwiftUI
9 9
 
10 10
 struct SidebarChargedDevicesSectionView: View {
11 11
     let title: String
12
+    let mode: ChargedDeviceLibraryMode
12 13
     let chargedDevices: [ChargedDeviceSummary]
13 14
     let emptyStateText: String
14 15
     let tint: Color
@@ -16,22 +17,43 @@ struct SidebarChargedDevicesSectionView: View {
16 17
 
17 18
     var body: some View {
18 19
         Section(header: headerView) {
19
-            if chargedDevices.isEmpty {
20
-                Text(emptyStateText)
21
-                    .font(.footnote)
22
-                    .foregroundColor(.secondary)
23
-                    .frame(maxWidth: .infinity, alignment: .leading)
24
-                    .padding(18)
25
-                    .meterCard(tint: tint, fillOpacity: 0.12, strokeOpacity: 0.18)
26
-            } else {
27
-                ForEach(chargedDevices) { chargedDevice in
28
-                    NavigationLink(destination: ChargedDeviceDetailView(chargedDeviceID: chargedDevice.id)) {
29
-                        ChargedDeviceSidebarCardView(chargedDevice: chargedDevice)
30
-                    }
31
-                    .buttonStyle(.plain)
20
+            // Library overview row — navigates to the full management library
21
+            NavigationLink(destination: SidebarChargedDeviceLibraryView(mode: mode)) {
22
+                libraryRow
23
+            }
24
+            .buttonStyle(.plain)
25
+
26
+            ForEach(chargedDevices) { chargedDevice in
27
+                NavigationLink(destination: ChargedDeviceDetailView(chargedDeviceID: chargedDevice.id)) {
28
+                    ChargedDeviceSidebarCardView(chargedDevice: chargedDevice)
32 29
                 }
30
+                .buttonStyle(.plain)
31
+            }
32
+        }
33
+    }
34
+
35
+    private var libraryRow: some View {
36
+        HStack(spacing: 12) {
37
+            Image(systemName: mode == .device ? "square.grid.2x2" : "bolt.circle")
38
+                .font(.system(size: 16, weight: .semibold))
39
+                .foregroundColor(tint)
40
+                .frame(width: 36, height: 36)
41
+                .background(tint.opacity(0.14))
42
+                .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
43
+
44
+            VStack(alignment: .leading, spacing: 2) {
45
+                Text("All \(title)")
46
+                    .font(.subheadline.weight(.semibold))
47
+                    .foregroundColor(.primary)
48
+                let count = chargedDevices.count
49
+                Text("\(count) \(count == 1 ? mode.singularTitle.lowercased() : mode.title.lowercased())")
50
+                    .font(.caption)
51
+                    .foregroundColor(.secondary)
33 52
             }
53
+
54
+            Spacer()
34 55
         }
56
+        .padding(.vertical, 4)
35 57
     }
36 58
 
37 59
     private var headerView: some View {
+2 -0
USB Meter/Views/Sidebar/SidebarView.swift
@@ -82,6 +82,7 @@ struct SidebarView: View {
82 82
 
83 83
             SidebarChargedDevicesSectionView(
84 84
                 title: "Devices",
85
+                mode: .device,
85 86
                 chargedDevices: appData.deviceSummaries,
86 87
                 emptyStateText: "No devices yet. Open Charge Record on a live meter or use the add button here to create one and start learning capacity.",
87 88
                 tint: .orange,
@@ -90,6 +91,7 @@ struct SidebarView: View {
90 91
 
91 92
             SidebarChargedDevicesSectionView(
92 93
                 title: "Chargers",
94
+                mode: .charger,
93 95
                 chargedDevices: appData.chargerSummaries,
94 96
                 emptyStateText: "No chargers yet. Add one here so wireless sessions can track both the charged device and the charger being used.",
95 97
                 tint: .pink,