← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


commit b29c19ff482498ff27a52d52c2b3797495961379
parent edcc9fdb76d8a4b9dd19c97286a98953c16de071
Author: Michael Camilleri <[email protected]>
Date:   Tue,  3 Mar 2026 16:49:30 +0900

Add settings screen

This project originally avoided having a settings screen but it was only
possible to hold out for so long. The settings screen allows a user to
change the heading that appears above the list (now defaults to 'Items')
to arbitrary text. A user can also toggle dark mode. A settings screen
was also a natural place to put an about screen so that's been added,
too.

Co-Authored-By: Claude 4.6 Opus <[email protected]>

Diffstat

MListless.xcodeproj/project.pbxproj | 12++++++++++++
MListlessMac/Listless.entitlements | 2--
MListlessiOS/Extensions/TaskListView+NavigationHeader.swift | 40+++++++++++++++++++++++++---------------
MListlessiOS/ListlessiOSApp.swift | 14++++++++++++++
AListlessiOS/Views/AboutView.swift | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AListlessiOS/Views/SettingsView.swift | 46++++++++++++++++++++++++++++++++++++++++++++++
AListlessiOS/Views/SyncDiagnosticsView.swift | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MListlessiOS/Views/TaskListView.swift | 127++++++++++++++++---------------------------------------------------------------
AMedia.xcassets/AboutIcon.imageset/Contents.json | 12++++++++++++
AMedia.xcassets/AboutIcon.imageset/Icon.png | 0

10 files changed, 295 insertions(+), 118 deletions(-)

diff --git a/Listless.xcodeproj/project.pbxproj b/Listless.xcodeproj/project.pbxproj

@@ -7,6 +7,8 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		060436CDDB388BC04C51581A /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E995954787F0A14CCFF348 /* AboutView.swift */; };
+		072594B24D88AD6D1DF7AFE5 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C611E04943F1D82D6F975592 /* SettingsView.swift */; };
 		0ACA67F6578EFF181EE5C9A7 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB8A3BEB346267B30B4675F /* TaskItem.swift */; };
 		0F12D56A528FCBF8A67864CB /* TappableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECE0E961F87BA32FA87BF90 /* TappableTextField.swift */; };
 		11AA75BE98CFBE44AEAB7100 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9404C09EE1A4D91DFF338464 /* Media.xcassets */; };
@@ -22,6 +24,7 @@
 		3ABE52A15C2059D8D5570528 /* TaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3DEE364304587D280C5672 /* TaskStore.swift */; };
 		3D1F551A03B97ECF4E3DC8B0 /* TaskRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E06485DBE35B60868E14202A /* TaskRowView.swift */; };
 		3EDB6A9A30B4226C15E7F44D /* AppCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C46FC97E6DB6FF81AC5C22 /* AppCommands.swift */; };
+		4DD2030E321567BD25661760 /* SyncDiagnosticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9288507CE6023425D1DE724 /* SyncDiagnosticsView.swift */; };
 		4E5A0A02121E02124F80E320 /* TaskListView+SyncUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7BD42B1E3C71333FA24893 /* TaskListView+SyncUI.swift */; };
 		5035EC4C7518A5FF9AD454CA /* TaskRowDragGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F416DD868A4C044F0D64F8D0 /* TaskRowDragGesture.swift */; };
 		5761B201BF46FCA9C5C98CEF /* PlatformScrollIndicatorsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466F9B0E407DF1F5B4789531 /* PlatformScrollIndicatorsModifier.swift */; };
@@ -126,11 +129,13 @@
 		B7588879D0FA1C2A8BCEF14F /* HoverCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverCursorModifier.swift; sourceTree = "<group>"; };
 		BFF7D84B54AE70036D205CA4 /* PullToCreate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToCreate.swift; sourceTree = "<group>"; };
 		C14858BDFD1FD5119F1F24A6 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
+		C611E04943F1D82D6F975592 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
 		C71466C5CD1A5BA984352F8D /* Listless iOS Unit Tests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = "Listless iOS Unit Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
 		C9B14DC786A336008AAB78EE /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; };
 		CB43816B8E7F083A2AD07F28 /* TaskListView+Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+Toolbar.swift"; sourceTree = "<group>"; };
 		D2C018476BD91B73870244B9 /* TaskListViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListViewProtocol.swift; sourceTree = "<group>"; };
 		D2D9CDDA8913CD116FB4DA74 /* TaskListView+PullGestures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+PullGestures.swift"; sourceTree = "<group>"; };
+		D3E995954787F0A14CCFF348 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
 		D43D37CE25806380C0B13466 /* BuildNumber.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = BuildNumber.xcconfig; sourceTree = "<group>"; };
 		D57C3CC81C4380EFAE4DB910 /* TaskRowDragGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowDragGesture.swift; sourceTree = "<group>"; };
 		D640E7D21735C62A30A26DA4 /* ClickableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClickableTextField.swift; sourceTree = "<group>"; };
@@ -139,6 +144,7 @@
 		E2174C43654733E9D4023157 /* PlatformTextFieldWidthModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformTextFieldWidthModifier.swift; sourceTree = "<group>"; };
 		E51B8129962E5CC78ECDDC2B /* TaskListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListView.swift; sourceTree = "<group>"; };
 		E8448C5778F75F52719114AF /* PlatformTextFieldWidthModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformTextFieldWidthModifier.swift; sourceTree = "<group>"; };
+		E9288507CE6023425D1DE724 /* SyncDiagnosticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDiagnosticsView.swift; sourceTree = "<group>"; };
 		F1E998119283F784B9ADEE28 /* AppColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppColors.swift; sourceTree = "<group>"; };
 		F416DD868A4C044F0D64F8D0 /* TaskRowDragGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowDragGesture.swift; sourceTree = "<group>"; };
 		FA5D58EC1FBAA96E83A79445 /* PlatformScrollIndicatorsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformScrollIndicatorsModifier.swift; sourceTree = "<group>"; };
@@ -207,8 +213,11 @@
 		58F917D865E0BDF4EF282306 /* Views */ = {
 			isa = PBXGroup;
 			children = (
+				D3E995954787F0A14CCFF348 /* AboutView.swift */,
 				567DBAC2A39FA2760D006AAB /* PullToClear.swift */,
 				BFF7D84B54AE70036D205CA4 /* PullToCreate.swift */,
+				C611E04943F1D82D6F975592 /* SettingsView.swift */,
+				E9288507CE6023425D1DE724 /* SyncDiagnosticsView.swift */,
 				E51B8129962E5CC78ECDDC2B /* TaskListView.swift */,
 				199CC2F58DD7CBA3F2229366 /* TaskRowView.swift */,
 			);
@@ -505,6 +514,7 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				060436CDDB388BC04C51581A /* AboutView.swift in Sources */,
 				BA6953E0EFE6F8255F05A3FD /* AccentColor.swift in Sources */,
 				5D3EE9526DA269EE9EE3AB52 /* AppColors.swift in Sources */,
 				3EDB6A9A30B4226C15E7F44D /* AppCommands.swift in Sources */,
@@ -519,6 +529,8 @@
 				77FE96F070B1F7FE31A9CE51 /* PlatformTextFieldWidthModifier.swift in Sources */,
 				785721EB774EAC6BBA26C038 /* PullToClear.swift in Sources */,
 				E47136CA7428927395D8C7C7 /* PullToCreate.swift in Sources */,
+				072594B24D88AD6D1DF7AFE5 /* SettingsView.swift in Sources */,
+				4DD2030E321567BD25661760 /* SyncDiagnosticsView.swift in Sources */,
 				0F12D56A528FCBF8A67864CB /* TappableTextField.swift in Sources */,
 				0ACA67F6578EFF181EE5C9A7 /* TaskItem.swift in Sources */,
 				1477B460E3DEFA3FDB7DA65B /* TaskListTypes.swift in Sources */,

diff --git a/ListlessMac/Listless.entitlements b/ListlessMac/Listless.entitlements

@@ -2,8 +2,6 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-	<key>com.apple.security.app-sandbox</key>
-	<true/>
 	<key>aps-environment</key>
 	<string>development</string>
 	<key>com.apple.developer.icloud-container-identifiers</key>

diff --git a/ListlessiOS/Extensions/TaskListView+NavigationHeader.swift b/ListlessiOS/Extensions/TaskListView+NavigationHeader.swift

@@ -2,21 +2,31 @@ import SwiftUI
 
 extension TaskListView {
     var navigationHeader: some View {
-        Text("Tasks")
-            .font(.largeTitle)
-            .fontWeight(.bold)
-            .frame(maxWidth: .infinity, alignment: .leading)
-            .padding(.horizontal, 16)
-            .padding(.bottom, 8)
-            .contentShape(Rectangle())
-            .onTapGesture {
-                selectedTaskID = nil
-                focusedField = .scrollView
+        HStack {
+            Text(headingText)
+                .font(.largeTitle)
+                .fontWeight(.bold)
+            Spacer()
+            Button {
+                showSettings()
+            } label: {
+                Image(systemName: "gearshape")
+                    .font(.title2)
+                    .foregroundStyle(.secondary)
             }
-            .simultaneousGesture(
-                TapGesture(count: 4).onEnded {
-                    showSyncDiagnostics()
-                }
-            )
+            .buttonStyle(.plain)
+        }
+        .padding(.horizontal, 16)
+        .padding(.bottom, 8)
+        .contentShape(Rectangle())
+        .onTapGesture {
+            selectedTaskID = nil
+            focusedField = .scrollView
+        }
+        .simultaneousGesture(
+            TapGesture(count: 4).onEnded {
+                showSyncDiagnostics()
+            }
+        )
     }
 }

diff --git a/ListlessiOS/ListlessiOSApp.swift b/ListlessiOS/ListlessiOSApp.swift

@@ -2,6 +2,7 @@ import SwiftUI
 
 @main
 struct ListlessiOSApp: App {
+    @AppStorage("appearanceMode") private var appearanceMode = 0
     private let persistenceController: PersistenceController
 
     init() {
@@ -19,6 +20,19 @@ struct ListlessiOSApp: App {
                     Color.clear.frame(height: 8)
                 }
                 .environment(\.managedObjectContext, persistenceController.viewContext)
+                .onChange(of: appearanceMode, initial: true) { _, newValue in
+                    let style: UIUserInterfaceStyle = switch newValue {
+                    case 1: .light
+                    case 2: .dark
+                    default: .unspecified
+                    }
+                    for scene in UIApplication.shared.connectedScenes {
+                        guard let windowScene = scene as? UIWindowScene else { continue }
+                        for window in windowScene.windows {
+                            window.overrideUserInterfaceStyle = style
+                        }
+                    }
+                }
                 .overlay(alignment: .top) {
                     Color.outerBackground
                         .opacity(0.9)

diff --git a/ListlessiOS/Views/AboutView.swift b/ListlessiOS/Views/AboutView.swift

@@ -0,0 +1,66 @@
+import SwiftUI
+
+struct AboutView: View {
+    private var appVersion: String {
+        Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
+    }
+
+    private var buildNumber: String {
+        Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
+    }
+
+    var body: some View {
+        List {
+            Section {
+                VStack(spacing: 12) {
+                    Image("AboutIcon")
+                        .resizable()
+                        .scaledToFit()
+                        .frame(width: 100, height: 100)
+                        .clipShape(RoundedRectangle(cornerRadius: 22))
+
+                    Text("Listless")
+                        .font(.title2)
+                        .fontWeight(.bold)
+
+                    Text("Version \(appVersion) (\(buildNumber))")
+                        .font(.subheadline)
+                        .foregroundStyle(.secondary)
+                        .padding(.bottom, 8)
+
+                    Text("Made in Tokyo by Michael Camilleri")
+                        .font(.headline)
+                        .foregroundStyle(.primary)
+                }
+                .frame(maxWidth: .infinity)
+                .padding(.vertical, 12)
+                .padding(.bottom, 4)
+                .listRowBackground(Color.clear)
+                .listRowSeparator(.hidden)
+            }
+
+            Section {
+                Link(destination: URL(string: "https://apps.inqk.net/listless")!) {
+                    Label("Website", systemImage: "globe")
+                }
+                Link(destination: URL(string: "https://github.com/pyrmont/listless")!) {
+                    Label("Source Code", systemImage: "chevron.left.forwardslash.chevron.right")
+                }
+            }
+
+            Section {
+                VStack(alignment: .leading, spacing: 12) {
+                    Text(
+                        "Thank you to my wife, daughter and sons for their love and inspiration."
+                    )
+                }
+                .font(.subheadline)
+                .foregroundStyle(.secondary)
+                .listRowBackground(Color.clear)
+                .listRowSeparator(.hidden)
+            }
+        }
+        .navigationTitle("About")
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}

diff --git a/ListlessiOS/Views/SettingsView.swift b/ListlessiOS/Views/SettingsView.swift

@@ -0,0 +1,46 @@
+import SwiftUI
+
+struct SettingsView: View {
+    @ObservedObject var syncMonitor: CloudKitSyncMonitor
+    @Environment(\.dismiss) private var dismiss
+    @AppStorage("headingText") private var headingText = "Items"
+    @AppStorage("appearanceMode") private var appearanceMode = 0
+
+    var body: some View {
+        NavigationStack {
+            List {
+                Section("Heading") {
+                    TextField("Heading", text: $headingText)
+                }
+
+                Section("Appearance") {
+                    Picker("Appearance", selection: $appearanceMode) {
+                        Text("System").tag(0)
+                        Text("Light").tag(1)
+                        Text("Dark").tag(2)
+                    }
+                    .pickerStyle(.segmented)
+                }
+
+                Section {
+                    NavigationLink("iCloud Diagnostics") {
+                        SyncDiagnosticsView(syncMonitor: syncMonitor)
+                    }
+                }
+
+                Section {
+                    NavigationLink("About") {
+                        AboutView()
+                    }
+                }
+            }
+            .navigationTitle("Settings")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button("Done") { dismiss() }
+                }
+            }
+        }
+    }
+}

diff --git a/ListlessiOS/Views/SyncDiagnosticsView.swift b/ListlessiOS/Views/SyncDiagnosticsView.swift

@@ -0,0 +1,94 @@
+import SwiftUI
+
+struct SyncDiagnosticsView: View {
+    @ObservedObject var syncMonitor: CloudKitSyncMonitor
+
+    private static let timestampFormatter: DateFormatter = {
+        let formatter = DateFormatter()
+        formatter.dateStyle = .none
+        formatter.timeStyle = .medium
+        return formatter
+    }()
+
+    var body: some View {
+        List {
+            Section("Status") {
+                row("Transient Banner", syncMonitor.transientErrorMessage ?? "None")
+                row("Last Error Domain", syncMonitor.lastCloudKitErrorDomain ?? "None")
+                row(
+                    "Last Error Code",
+                    syncMonitor.lastCloudKitErrorCode.map(String.init) ?? "None"
+                )
+                row("Last Error Description", syncMonitor.lastCloudKitErrorDescription ?? "None")
+                row(
+                    "Last Success",
+                    syncMonitor.lastSuccessfulSyncDate.map(Self.timestampFormatter.string(from:))
+                        ?? "None"
+                )
+            }
+
+            Section("Recent Events") {
+                if syncMonitor.recentDiagnostics.isEmpty {
+                    Text("No events captured yet.")
+                        .foregroundStyle(.secondary)
+                } else {
+                    ForEach(syncMonitor.recentDiagnostics.reversed()) { entry in
+                        VStack(alignment: .leading, spacing: 4) {
+                            Text(
+                                "\(Self.timestampFormatter.string(from: entry.timestamp)) [\(entry.level.uppercased())]"
+                            )
+                            .font(.caption.monospaced())
+                            .foregroundStyle(.secondary)
+
+                            Text(entry.message)
+                                .font(.caption.monospaced())
+                                .textSelection(.enabled)
+                        }
+                        .padding(.vertical, 2)
+                    }
+                }
+            }
+        }
+        .navigationTitle("Sync Diagnostics")
+        .navigationBarTitleDisplayMode(.inline)
+        .toolbar {
+            ToolbarItem(placement: .topBarTrailing) {
+                Button("Copy") {
+                    UIPasteboard.general.string = diagnosticDump
+                }
+            }
+        }
+    }
+
+    @ViewBuilder
+    private func row(_ title: String, _ value: String) -> some View {
+        VStack(alignment: .leading, spacing: 4) {
+            Text(title)
+                .font(.caption)
+                .foregroundStyle(.secondary)
+            Text(value)
+                .font(.body.monospaced())
+                .textSelection(.enabled)
+        }
+        .padding(.vertical, 2)
+    }
+
+    private var diagnosticDump: String {
+        var lines: [String] = []
+        lines.append("Transient Banner: \(syncMonitor.transientErrorMessage ?? "None")")
+        lines.append("Last Error Domain: \(syncMonitor.lastCloudKitErrorDomain ?? "None")")
+        lines.append("Last Error Code: \(syncMonitor.lastCloudKitErrorCode.map(String.init) ?? "None")")
+        lines.append("Last Error Description: \(syncMonitor.lastCloudKitErrorDescription ?? "None")")
+        lines.append(
+            "Last Success: \(syncMonitor.lastSuccessfulSyncDate.map(Self.timestampFormatter.string(from:)) ?? "None")"
+        )
+        lines.append("")
+        lines.append("Recent Events:")
+        for entry in syncMonitor.recentDiagnostics {
+            lines.append(
+                "\(Self.timestampFormatter.string(from: entry.timestamp)) [\(entry.level.uppercased())] \(entry.message)"
+            )
+        }
+        return lines.joined(separator: "\n")
+    }
+}

diff --git a/ListlessiOS/Views/TaskListView.swift b/ListlessiOS/Views/TaskListView.swift

@@ -14,6 +14,7 @@ struct TaskListView: View, TaskListViewProtocol {
         var pullUpOffset: CGFloat = 0
         var isDragging: Bool = false
         var isShowingSyncDiagnostics = false
+        var isShowingSettings = false
         var clearingTaskIDs: Set<UUID> = []
         var rowFrames: [UUID: CGRect] = [:]
     }
@@ -22,6 +23,7 @@ struct TaskListView: View, TaskListViewProtocol {
         var refreshID = UUID()
     }
 
+    @AppStorage("headingText") var headingText = "Items"
     @Environment(\.undoManager) var undoManager
     @Environment(\.managedObjectContext) var managedObjectContext
 
@@ -152,6 +154,10 @@ struct TaskListView: View, TaskListViewProtocol {
         iState.isShowingSyncDiagnostics = true
     }
 
+    func showSettings() {
+        iState.isShowingSettings = true
+    }
+
     var body: some View {
         taskScrollView
             .contentShape(Rectangle())
@@ -202,7 +208,22 @@ struct TaskListView: View, TaskListViewProtocol {
             }
             .focusedSceneValue(\.taskActions, currentTaskActions)
             .sheet(isPresented: isShowingSyncDiagnosticsStateBinding) {
-                SyncDiagnosticsView(syncMonitor: syncMonitor)
+                NavigationStack {
+                    SyncDiagnosticsView(syncMonitor: syncMonitor)
+                        .toolbar {
+                            ToolbarItem(placement: .topBarLeading) {
+                                Button("Close") { iState.isShowingSyncDiagnostics = false }
+                            }
+                        }
+                }
+            }
+            .sheet(
+                isPresented: Binding(
+                    get: { iState.isShowingSettings },
+                    set: { iState.isShowingSettings = $0 }
+                )
+            ) {
+                SettingsView(syncMonitor: syncMonitor)
             }
             .alert(
                 item: Binding(
@@ -304,7 +325,10 @@ struct TaskListView: View, TaskListViewProtocol {
                 fState.focusedField = newValue
                 handleFocusChange(from: oldValue, to: newValue)
 
-                if newValue == nil {
+                if newValue == nil,
+                    !iState.isShowingSettings,
+                    !iState.isShowingSyncDiagnostics
+                {
                     if let pending = pendingFocus {
                         focusedFieldBinding = pending
                         fState.focusedField = pending
@@ -370,102 +394,3 @@ struct TaskListView: View, TaskListViewProtocol {
         )
     }
 }
-
-private struct SyncDiagnosticsView: View {
-    @ObservedObject var syncMonitor: CloudKitSyncMonitor
-    @Environment(\.dismiss) private var dismiss
-
-    private static let timestampFormatter: DateFormatter = {
-        let formatter = DateFormatter()
-        formatter.dateStyle = .none
-        formatter.timeStyle = .medium
-        return formatter
-    }()
-
-    var body: some View {
-        NavigationStack {
-            List {
-                Section("Status") {
-                    row("Transient Banner", syncMonitor.transientErrorMessage ?? "None")
-                    row("Last Error Domain", syncMonitor.lastCloudKitErrorDomain ?? "None")
-                    row(
-                        "Last Error Code",
-                        syncMonitor.lastCloudKitErrorCode.map(String.init) ?? "None"
-                    )
-                    row("Last Error Description", syncMonitor.lastCloudKitErrorDescription ?? "None")
-                    row(
-                        "Last Success",
-                        syncMonitor.lastSuccessfulSyncDate.map(Self.timestampFormatter.string(from:))
-                            ?? "None"
-                    )
-                }
-
-                Section("Recent Events") {
-                    if syncMonitor.recentDiagnostics.isEmpty {
-                        Text("No events captured yet.")
-                            .foregroundStyle(.secondary)
-                    } else {
-                        ForEach(syncMonitor.recentDiagnostics.reversed()) { entry in
-                            VStack(alignment: .leading, spacing: 4) {
-                                Text(
-                                    "\(Self.timestampFormatter.string(from: entry.timestamp)) [\(entry.level.uppercased())]"
-                                )
-                                .font(.caption.monospaced())
-                                .foregroundStyle(.secondary)
-
-                                Text(entry.message)
-                                    .font(.caption.monospaced())
-                                    .textSelection(.enabled)
-                            }
-                            .padding(.vertical, 2)
-                        }
-                    }
-                }
-            }
-            .navigationTitle("Sync Diagnostics")
-            .navigationBarTitleDisplayMode(.inline)
-            .toolbar {
-                ToolbarItem(placement: .topBarLeading) {
-                    Button("Close") { dismiss() }
-                }
-                ToolbarItem(placement: .topBarTrailing) {
-                    Button("Copy") {
-                        UIPasteboard.general.string = diagnosticDump
-                    }
-                }
-            }
-        }
-    }
-
-    @ViewBuilder
-    private func row(_ title: String, _ value: String) -> some View {
-        VStack(alignment: .leading, spacing: 4) {
-            Text(title)
-                .font(.caption)
-                .foregroundStyle(.secondary)
-            Text(value)
-                .font(.body.monospaced())
-                .textSelection(.enabled)
-        }
-        .padding(.vertical, 2)
-    }
-
-    private var diagnosticDump: String {
-        var lines: [String] = []
-        lines.append("Transient Banner: \(syncMonitor.transientErrorMessage ?? "None")")
-        lines.append("Last Error Domain: \(syncMonitor.lastCloudKitErrorDomain ?? "None")")
-        lines.append("Last Error Code: \(syncMonitor.lastCloudKitErrorCode.map(String.init) ?? "None")")
-        lines.append("Last Error Description: \(syncMonitor.lastCloudKitErrorDescription ?? "None")")
-        lines.append(
-            "Last Success: \(syncMonitor.lastSuccessfulSyncDate.map(Self.timestampFormatter.string(from:)) ?? "None")"
-        )
-        lines.append("")
-        lines.append("Recent Events:")
-        for entry in syncMonitor.recentDiagnostics {
-            lines.append(
-                "\(Self.timestampFormatter.string(from: entry.timestamp)) [\(entry.level.uppercased())] \(entry.message)"
-            )
-        }
-        return lines.joined(separator: "\n")
-    }
-}

diff --git a/Media.xcassets/AboutIcon.imageset/Contents.json b/Media.xcassets/AboutIcon.imageset/Contents.json

@@ -0,0 +1,12 @@
+{
+  "images" : [
+    {
+      "filename" : "Icon.png",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

diff --git a/Media.xcassets/AboutIcon.imageset/Icon.png b/Media.xcassets/AboutIcon.imageset/Icon.png

Binary files differ.