commit 6052bcda58824407345a9209cd85dbc42b938e50
parent 7d49fea5175899cdb299b4362634e9f370b2b828
Author: Michael Camilleri <[email protected]>
Date: Wed, 4 Mar 2026 09:24:40 +0900
Update AGENTS.md regarding new views
Diffstat:
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -7,7 +7,7 @@
- `Listless/Extensions` holds extensions on shared types; `TaskListView+Logic.swift` and `TaskListView+SyncUI.swift` are extensions on `TaskListViewProtocol` (not the concrete struct) so SourceKit can resolve them unambiguously across both targets.
- `Listless/Helpers` holds shared non-view supporting code (`AccentColor`, `KeyboardNavigationModifier`, `TaskListTypes`, `TaskListViewProtocol`). `TaskListTypes.swift` defines the `FocusField` and `DragState` enums as top-level types (shared by both platform `TaskListView` structs). `TaskListViewProtocol.swift` defines the `@MainActor TaskListViewProtocol` that both structs conform to, declaring the shared property contract (`tasks`, `store`, `syncMonitor`, `managedObjectContext`, `focusedField`, `selectedTaskID`, `pendingFocus`, `dragState`, `didStartDrag()`).
- `ListlessiOS/` contains the iOS app entry point, organised into three subdirectories:
- - `Views/` — iOS-specific view components (`TaskListView`, `TaskRowView`, `PullToCreate`).
+ - `Views/` — iOS-specific view components (`TaskListView`, `TaskRowView`, `PullToCreate`, `SettingsView`, `SyncDiagnosticsView`, `AboutView`).
- `Helpers/` — gesture recognizers, UIKit representables, color definitions, and platform-shim view modifiers (`TappableTextField`, `TaskRowSwipeGesture`, `TaskRowDragGesture`, `AppColors`, `HoverCursorModifier`, etc.).
- `Extensions/` — platform-specific extensions on shared types (`TaskListView+NavigationHeader`, `TaskListView+Toolbar`, `TaskListView+PullToCreate`, `TaskListView+Drag`).
- `ListlessMac/` contains the macOS app entry point with the same three-subdirectory structure as `ListlessiOS/` (`Views/` includes `TaskListView`); an excluded `AppKit/` subdirectory holds the reverted AppKit implementation.
@@ -58,6 +58,7 @@
- `TaskItem` is an `NSManagedObject` subclass with auto-updating `updatedAt` timestamp via `willSave()`.
- CloudKit container identifier is `iCloud.net.inqk.listless`; entitlements are defined in `project.yml` and generated by XcodeGen.
- Keep CloudKit configuration and Core Data setup inside `Listless/Sync`; exposing raw Core Data contexts elsewhere is discouraged.
+- `KeyValueSyncBridge` (`Listless/Sync/KeyValueSyncBridge.swift`) bridges `@AppStorage` keys to `NSUbiquitousKeyValueStore` for iCloud sync of user preferences. Currently syncs `"headingText"` (the customisable list title). On startup it pulls cloud values into `UserDefaults`; bidirectional observation keeps the two stores in sync with an `isSyncing` flag to prevent feedback loops. To sync additional preferences, add the key to the `keys` set in `ListlessiOSApp.init()`.
- When adding fields to the Core Data model, update `Listless.xcdatamodeld`, add migration mappings if needed, and document changes in `Docs/Schema.md`.
## Testing Guidelines
@@ -92,7 +93,11 @@
- `isDragging` is passed as `@Binding var isDragging: Bool` all the way from `TaskListView` (`@State var isDragging: Bool`, iOS-only, declared in `ListlessiOS/Views/TaskListView.swift`) through `TaskRowView` to `TaskRowSwipeGesture`. The `@Binding` drives both the `including: isDragging ? .none : .all` gesture mask (disables recognition entirely during drag reordering) and the `guard !isDragging` check inside `onChanged`.
- `isActive` and `isEditing` were removed from the gesture interface — `isActive` was always `true` at the call site (the gesture is only applied to active task rows in the ForEach), and `isEditing` can never be true during an active swipe (you can't initiate a horizontal pan while the keyboard is up).
- **Incremental build**: iOS views are built in stages. The stub accepts the full interface; behaviour is filled in progressively. Keep the init signature in sync with macOS when adding parameters.
-- **Title and navigation**: `ListlessiOSApp.swift` uses a plain `WindowGroup` with no `NavigationStack`. The navigation header title ("Tasks") is rendered as a SwiftUI `Text` inside the `ScrollView` via the `navigationHeader` computed property defined in `ListlessiOS/Extensions/TaskListView+NavigationHeader.swift`; the macOS `body` in `ListlessMac/Views/TaskListView.swift` simply omits the call, so no macOS stub is needed. A `.overlay(alignment: .top)` of `Color.outerBackground.opacity(0.9).ignoresSafeArea(edges: .top).frame(height: 0)` covers the status bar region so scroll content doesn't show under the clock and system icons.
+- **Title and navigation**: `ListlessiOSApp.swift` uses a plain `WindowGroup` with no `NavigationStack`. The navigation header title is customisable via `@AppStorage("headingText")` (default `"Items"`, synced to iCloud via `KeyValueSyncBridge`). It is rendered as a SwiftUI `Text` inside the `ScrollView` via the `navigationHeader` computed property defined in `ListlessiOS/Extensions/TaskListView+NavigationHeader.swift`; the macOS `body` in `ListlessMac/Views/TaskListView.swift` simply omits the call, so no macOS stub is needed. A `.overlay(alignment: .top)` of `Color.outerBackground.opacity(0.9).ignoresSafeArea(edges: .top).frame(height: 0)` covers the status bar region so scroll content doesn't show under the clock and system icons.
+- **Settings screen**: `SettingsView` (`ListlessiOS/Views/SettingsView.swift`) is presented as a sheet from the gear icon in the navigation header. Contains four sections: list title (`TextField` bound to `@AppStorage("headingText")`), appearance picker (segmented: System/Light/Dark, bound to `@AppStorage("appearanceMode")`), `NavigationLink` to `SyncDiagnosticsView`, and `NavigationLink` to `AboutView`. The `syncMonitor` is passed in from `TaskListView`'s sheet presentation.
+- **Appearance override**: Uses `UIWindow.overrideUserInterfaceStyle` (set via `onChange(of: appearanceMode, initial: true)` in `ListlessiOSApp`) rather than `.preferredColorScheme()`, because `.preferredColorScheme(nil)` does not properly revert from a non-nil value in sheets. The UIKit window-level override affects all presented view controllers including sheets, and `.unspecified` correctly reverts to the system setting.
+- **Focus guard for sheets**: The `onChange(of: focusedFieldBinding)` handler in `TaskListView` skips the "reclaim focus to `.scrollView`" logic when `isShowingSettings` or `isShowingSyncDiagnostics` is true, preventing the focus machinery from stealing focus from TextFields in presented sheets.
+- **App icon in About screen**: `Media.xcassets/AboutIcon.imageset` contains a copy of the app icon as a regular image set, because `.appiconset` images cannot be loaded via `Image("AppIcon")` in SwiftUI. Reference it as `Image("AboutIcon")`.
- **iOS color system**: `ListlessiOS/Helpers/AppColors.swift` defines `Color.outerBackground` and `Color.taskCard` as adaptive `UIColor`-backed colors (warm gray/black outer; white/dark-gray card). Adjust these two values to shift the overall palette.
- **iOS card layout**: Active task rows use `UnevenRoundedRectangle` — square leading corners (cards extend flush to the left screen edge, no leading margin) and 14pt trailing corners. Completed rows have no card background and sit directly on `outerBackground`. VStack spacing (12pt) and padding (trailing + vertical) are set directly in `ListlessiOS/Views/TaskListView.swift`; the macOS `TaskListView.swift` uses 0 spacing and no padding.
- **Gradient accent bars**: Each active row has an 8pt `Rectangle` on the leading edge filled with `cachedAccentColor` — a gradient interpolated across HSB color stops (coral → magenta → purple-blue) based on `index`/`totalTasks`. Color is computed once on appear and recomputed via `onChange(of: "\(index)-\(totalTasks)")`. The same gradient is used as the swipe-right reveal color via the `completeColor` parameter on `taskSwipeGesture`. The color stops and interpolation logic are identical to the macOS version in `ListlessMac/Views/TaskRowView.swift`. The shared interpolation function lives in `Listless/Helpers/AccentColor.swift`.