Add dark mode support and design system foundation

- Create DesignSystem with ColorTokens, AppearanceManager, spacing, and typography
- Add appearance picker in Settings (System/Light/Dark modes)
- Replace hardcoded colors with design tokens in MainTabView and Enums+UI
- Inject AppearanceManager via environment in PlantGuideApp
- Add FEATURE_ROADMAP.md documenting 8 planned features

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-23 14:17:56 -06:00
parent 136b327093
commit be0d298d9f
8 changed files with 882 additions and 6 deletions

545
Docs/FEATURE_ROADMAP.md Normal file
View File

@@ -0,0 +1,545 @@
# PlantGuide Feature Roadmap
Implementation plan for 8 new features: Dark Mode, CloudKit Sync, Plant Rooms, Animations, Flexible Snoozing, Today View, Batch Actions, and Progress Photos.
---
## Requirements Summary
| Feature | Specification |
|---------|---------------|
| **Dark Mode** | System-following + manual toggle, new color token system |
| **CloudKit** | `NSPersistentCloudKitContainer`, fresh installs only |
| **Rooms** | User-creatable `Room` entity, defaults: Kitchen, Living Room, Bedroom, Bathroom, Office, Patio/Balcony, Other |
| **Snoozing** | 3 days, 1 week, 2 weeks, 1 month options |
| **Today View** | Replaces Care tab, shows overdue + today's tasks |
| **Batch Actions** | "Water all" for room or selection, logs per-plant |
| **Progress Photos** | Scheduled reminders, time-lapse with user-controllable speed slider |
| **Animations** | Subtle, defining—specified per context below |
**Tab structure after implementation:**
Camera | Browse | Collection | Today | Settings
---
## Implementation Order
| Phase | Feature | Effort | Notes |
|-------|---------|--------|-------|
| **1** | Dark Mode + Color Tokens | S | Foundation for all UI |
| **2** | CloudKit Sync | M | Must happen before new entities to avoid migration pain |
| **3** | Plant Rooms/Zones | M | New `Room` entity (synced), user-creatable with defaults |
| **4** | Subtle Animations | M | Design system completion |
| **5** | Flexible Snoozing | S | Extend existing CareTask system |
| **6** | Today View | M | Replace Care tab |
| **7** | Batch Actions | S | Builds on rooms |
| **8** | Progress Photos | L | New entity, reminders, time-lapse playback |
---
## Dependency Graph
```
Dark Mode (color tokens) ─────────────────┐
├──► All UI features depend on this
Subtle Animations ────────────────────────┘
Plant Rooms/Zones ──► Batch Actions ("Water all in Kitchen")
──► Today View (group by room)
Progress Photos ──► Independent (but benefits from animations)
Flexible Snoozing ──► Independent (extends existing system)
Today View ──► Requires rooms to be meaningful
──► Benefits from animations
```
---
## Phase 1: Dark Mode + Color Tokens
### Technical Approach
Create a centralized design system with semantic color tokens that respond to color scheme.
### Files to Create
| File | Purpose |
|------|---------|
| `Core/DesignSystem/ColorTokens.swift` | Semantic colors (background, surface, textPrimary, textSecondary, accent, destructive, etc.) |
| `Core/DesignSystem/AppearanceManager.swift` | `@Observable` class managing appearance preference (system/light/dark) |
| `Core/DesignSystem/DesignSystem.swift` | Namespace for tokens + spacing + typography |
### Files to Modify
| File | Change |
|------|--------|
| `PlantGuideApp.swift` | Inject `AppearanceManager`, apply `.preferredColorScheme()` |
| `MainTabView.swift` | Replace `.tint(.green)` with `DesignSystem.Colors.accent` |
| `SettingsView.swift` | Add appearance picker (System/Light/Dark) |
| All Views | Replace hardcoded colors with semantic tokens |
### Color Token Structure
```swift
enum Colors {
static let background = Color("Background") // Primary background
static let surface = Color("Surface") // Cards, sheets
static let textPrimary = Color("TextPrimary")
static let textSecondary = Color("TextSecondary")
static let accent = Color("Accent") // Green
static let destructive = Color("Destructive")
static let taskWatering = Color("TaskWatering")
static let taskFertilizing = Color("TaskFertilizing")
// ... etc
}
```
### Risks & Mitigations
| Risk | Mitigation |
|------|------------|
| Inconsistent adoption | Grep for hardcoded `Color(` and `.foregroundColor` after implementation |
| Asset catalog bloat | Use programmatic colors with `UIColor { traitCollection in }` |
### Tests
- Unit: `AppearanceManager` persists preference correctly
- UI: Screenshot tests for light/dark variants of key screens
---
## Phase 2: CloudKit Sync
### Technical Approach
Replace `NSPersistentContainer` with `NSPersistentCloudKitContainer`. Enable automatic sync.
### Files to Modify
| File | Change |
|------|--------|
| `CoreDataStack.swift` | Switch to `NSPersistentCloudKitContainer`, configure CloudKit container |
| `PlantGuideModel.xcdatamodeld` | Enable "Used with CloudKit" on configuration |
| `PlantGuide.entitlements` | Add CloudKit entitlement + iCloud container |
| `Info.plist` | Add `NSUbiquitousContainerIdentifier` if needed |
### Xcode Configuration
1. Enable CloudKit capability in Signing & Capabilities
2. Create CloudKit container: `iCloud.com.yourteam.PlantGuide`
3. Enable "CloudKit" checkbox on Core Data model configuration
### Schema Considerations
- All entities must have no unique constraints (CloudKit limitation)
- All relationships must have inverses
- No `Undefined` attribute types
- Optional attributes for any field that might be nil during sync
### Risks & Mitigations
| Risk | Mitigation |
|------|------------|
| Sync conflicts | CloudKit uses last-writer-wins; acceptable for this app |
| Large photos slow sync | Store photos in `CKAsset` (handled automatically by Core Data) |
| Offline-first gaps | `NSPersistentCloudKitContainer` handles this automatically |
| Rate limiting | Monitor CloudKit dashboard; unlikely for personal use |
### Tests
- Integration: Create plant on device A, verify appears on device B
- Unit: Mock `NSPersistentCloudKitContainer` for offline behavior
---
## Phase 3: Plant Rooms/Zones
### Technical Approach
New `Room` entity with one-to-many relationship to `Plant`. Replace `PlantLocation` enum usage.
### Files to Create
| File | Purpose |
|------|---------|
| `Domain/Entities/Room.swift` | `Room` struct (id, name, icon, sortOrder, isDefault) |
| `Domain/UseCases/Room/CreateDefaultRoomsUseCase.swift` | Creates 7 default rooms on first launch |
| `Domain/UseCases/Room/ManageRoomsUseCase.swift` | CRUD for rooms |
| `Domain/RepositoryInterfaces/RoomRepositoryProtocol.swift` | Repository protocol |
| `Data/Repositories/CoreDataRoomRepository.swift` | Core Data implementation |
| `Presentation/Scenes/Rooms/RoomsListView.swift` | Settings screen to manage rooms |
| `Presentation/Scenes/Rooms/RoomEditorView.swift` | Create/edit room |
| `Presentation/Scenes/Rooms/RoomsViewModel.swift` | ViewModel |
### Files to Modify
| File | Change |
|------|--------|
| `PlantGuideModel.xcdatamodeld` | Add `RoomMO` entity, relationship to `PlantMO` |
| `Plant.swift` | Change `location: PlantLocation?` to `roomID: UUID?` |
| `PlantMO` | Add `room` relationship |
| `CollectionView.swift` | Add room filter/grouping option |
| `PlantDetailView.swift` | Room picker instead of location enum |
| `SettingsView.swift` | Add "Manage Rooms" row |
| `DIContainer.swift` | Register room repository and use cases |
### Default Rooms
```swift
let defaults = [
Room(name: "Kitchen", icon: "refrigerator", sortOrder: 0, isDefault: true),
Room(name: "Living Room", icon: "sofa", sortOrder: 1, isDefault: true),
Room(name: "Bedroom", icon: "bed.double", sortOrder: 2, isDefault: true),
Room(name: "Bathroom", icon: "shower", sortOrder: 3, isDefault: true),
Room(name: "Office", icon: "desktopcomputer", sortOrder: 4, isDefault: true),
Room(name: "Patio/Balcony", icon: "sun.max", sortOrder: 5, isDefault: true),
Room(name: "Other", icon: "square.grid.2x2", sortOrder: 6, isDefault: true),
]
```
### Risks & Mitigations
| Risk | Mitigation |
|------|------------|
| Orphaned plants when room deleted | Cascade to "Other" room or prompt user |
| Migration from `PlantLocation` enum | Map existing enum values to default rooms in migration |
### Tests
- Unit: `CreateDefaultRoomsUseCase` creates exactly 7 rooms
- Unit: Room deletion cascades plants correctly
- UI: Room picker appears in plant detail
---
## Phase 4: Subtle Animations
### Technical Approach
Create reusable animation modifiers and view extensions. Apply consistently.
### Files to Create
| File | Purpose |
|------|---------|
| `Core/DesignSystem/Animations.swift` | Standard timing curves, durations |
| `Presentation/Common/Modifiers/FadeInModifier.swift` | Staggered fade-in for lists |
| `Presentation/Common/Modifiers/TaskCompleteModifier.swift` | Checkmark + confetti burst |
| `Presentation/Common/Modifiers/WateringFeedbackModifier.swift` | Water droplet animation |
| `Presentation/Common/Modifiers/PlantGrowthModifier.swift` | Subtle scale pulse |
| `Presentation/Common/Components/SuccessCheckmark.swift` | Animated checkmark component |
### Animation Specifications
| Context | Animation |
|---------|-----------|
| **Task complete** | Checkmark draws in (0.3s), row scales down + fades (0.2s), optional confetti particles |
| **Watering feedback** | 3 water droplets fall and fade (0.5s staggered) |
| **List appearance** | Staggered fade-in, 0.05s delay per item, max 10 items animated |
| **Tab switch** | Cross-fade (0.2s) |
| **Card press** | Scale to 0.97 on press, spring back |
| **Pull to refresh** | Plant icon rotates while loading |
| **Empty state** | Gentle float animation on illustration |
### Timing Standards
```swift
enum Animations {
static let quick = Animation.easeOut(duration: 0.15)
static let standard = Animation.easeInOut(duration: 0.25)
static let emphasis = Animation.spring(response: 0.4, dampingFraction: 0.7)
static let stagger = 0.05 // seconds between items
}
```
### Files to Modify
| File | Change |
|------|--------|
| `CareTaskRow.swift` | Add completion animation |
| `CollectionView.swift` | Staggered list animation |
| `MainTabView.swift` | Tab transition |
| All cards/buttons | Press feedback |
### Risks & Mitigations
| Risk | Mitigation |
|------|------------|
| Animation jank on older devices | Use `Animation.interactiveSpring` which degrades gracefully |
| Overdone animations | Keep durations under 0.5s, use subtle scales (0.95-1.05 range) |
| Accessibility | Respect `UIAccessibility.isReduceMotionEnabled` |
### Tests
- Unit: Animations respect reduce motion setting
- Manual: Test on oldest supported device (iPhone XR or similar)
---
## Phase 5: Flexible Snoozing
### Technical Approach
Extend existing snooze system with new intervals. Add snooze picker UI.
### Files to Create
| File | Purpose |
|------|---------|
| `Presentation/Common/Components/SnoozePickerView.swift` | Bottom sheet with snooze options |
### Files to Modify
| File | Change |
|------|--------|
| `CareTask.swift` | Add `snoozedUntil: Date?` property, `snoozeCount: Int` |
| `CareTaskRow.swift` | Replace swipe actions with snooze picker trigger |
| `CareScheduleViewModel.swift` | Update `snoozeTask()` to accept `SnoozeInterval` enum |
| `NotificationService.swift` | Reschedule notification on snooze |
| `CareTaskMO` | Add `snoozedUntil` and `snoozeCount` attributes |
### Snooze Options
```swift
enum SnoozeInterval: CaseIterable {
case threeDays
case oneWeek
case twoWeeks
case oneMonth
var days: Int {
switch self {
case .threeDays: return 3
case .oneWeek: return 7
case .twoWeeks: return 14
case .oneMonth: return 30
}
}
var label: String {
switch self {
case .threeDays: return "3 days"
case .oneWeek: return "1 week"
case .twoWeeks: return "2 weeks"
case .oneMonth: return "1 month"
}
}
}
```
### UI Behavior
- Swipe left on task → shows snooze picker (bottom sheet)
- Picker shows 4 options + "Custom date" option
- Snoozed tasks show "Snoozed until [date]" badge
- Snoozed tasks appear in "Snoozed" filter section
### Risks & Mitigations
| Risk | Mitigation |
|------|------------|
| Users abuse snooze | Track `snoozeCount`, optionally show "frequently snoozed" indicator |
| Notification sync | Cancel old notification, schedule new one in same transaction |
### Tests
- Unit: `snoozeTask(interval:)` calculates correct future date
- Unit: Snooze count increments
- UI: Snoozed task appears in correct section
---
## Phase 6: Today View
### Technical Approach
New view replacing Care tab. Dashboard with overdue + today sections, grouped by room.
### Files to Create
| File | Purpose |
|------|---------|
| `Presentation/Scenes/TodayView/TodayView.swift` | Main dashboard |
| `Presentation/Scenes/TodayView/TodayViewModel.swift` | ViewModel |
| `Presentation/Scenes/TodayView/Components/TaskSection.swift` | Overdue/Today section |
| `Presentation/Scenes/TodayView/Components/RoomTaskGroup.swift` | Tasks grouped by room |
| `Presentation/Scenes/TodayView/Components/QuickStatsBar.swift` | Today's progress (3/7 tasks done) |
### Files to Modify
| File | Change |
|------|--------|
| `MainTabView.swift` | Replace Care tab with Today tab |
| `DIContainer.swift` | Add `makeTodayViewModel()` |
### Today View Layout
```
┌─────────────────────────────────┐
│ Good morning! │
│ 3 of 7 tasks completed │
├─────────────────────────────────┤
│ OVERDUE (2) │
│ ┌─────────────────────────────┐ │
│ │ Monstera - Water (2d ago) │ │
│ │ Fern - Fertilize (1d ago) │ │
│ └─────────────────────────────┘ │
├─────────────────────────────────┤
│ TODAY (5) │
│ │
│ Kitchen (2) │
│ ┌─────────────────────────────┐ │
│ │ Pothos - Water │ │
│ │ Basil - Water │ │
│ └─────────────────────────────┘ │
│ │
│ Living Room (3) │
│ ┌─────────────────────────────┐ │
│ │ Snake Plant - Water │ │
│ │ Spider Plant - Fertilize │ │
│ │ Cactus - Water │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
```
### Risks & Mitigations
| Risk | Mitigation |
|------|------------|
| Empty state confusion | Show encouraging empty state: "All caught up!" |
| Performance with many tasks | Limit visible tasks, add "Show all" expansion |
### Tests
- Unit: Tasks correctly grouped by overdue/today/room
- UI: Empty state displays correctly
- UI: Task completion updates stats bar
---
## Phase 7: Batch Actions
### Technical Approach
Multi-select mode + batch action bar. Actions apply to selection or entire room.
### Files to Create
| File | Purpose |
|------|---------|
| `Presentation/Common/Components/BatchActionBar.swift` | Floating action bar |
| `Domain/UseCases/PlantCare/BatchCompleteTasksUseCase.swift` | Complete multiple tasks |
### Files to Modify
| File | Change |
|------|--------|
| `TodayView.swift` | Add edit mode, selection state |
| `TodayViewModel.swift` | `selectedTaskIDs: Set<UUID>`, batch action methods |
| `RoomTaskGroup.swift` | "Water all in [Room]" button |
| `CareScheduleRepositoryProtocol.swift` | Add `batchUpdate(taskIDs:completion:)` |
### UI Behavior
1. **Room-level batch**: Each room group has "Water all" button (only for watering tasks)
2. **Selection mode**: Long-press or Edit button enables multi-select
3. **Batch bar**: Appears at bottom when items selected: "Complete (5)" button
4. **Logging**: Each plant gets individual `CareTask` completion entry
### Batch Action Bar
```
┌─────────────────────────────────┐
│ 5 selected [Complete] [Cancel]│
└─────────────────────────────────┘
```
### Risks & Mitigations
| Risk | Mitigation |
|------|------------|
| Accidental batch complete | Require confirmation for >3 tasks |
| Mixed task types in selection | Only show "Complete" (generic), not task-specific action |
### Tests
- Unit: `BatchCompleteTasksUseCase` creates individual completion records
- UI: Selection count updates correctly
- UI: Confirmation appears for large batches
---
## Phase 8: Progress Photos
### Technical Approach
New `ProgressPhoto` entity linked to `Plant`. Scheduled reminders for capture. Time-lapse viewer with speed control.
### Files to Create
| File | Purpose |
|------|---------|
| `Domain/Entities/ProgressPhoto.swift` | Photo entity (id, plantID, imageData, dateTaken, notes) |
| `Domain/RepositoryInterfaces/ProgressPhotoRepositoryProtocol.swift` | Repository protocol |
| `Data/Repositories/CoreDataProgressPhotoRepository.swift` | Core Data implementation |
| `Domain/UseCases/Photos/CaptureProgressPhotoUseCase.swift` | Save photo with metadata |
| `Domain/UseCases/Photos/SchedulePhotoReminderUseCase.swift` | Weekly/monthly reminder |
| `Presentation/Scenes/ProgressPhotos/ProgressPhotoGalleryView.swift` | Grid of photos for plant |
| `Presentation/Scenes/ProgressPhotos/ProgressPhotoCaptureView.swift` | Camera capture screen |
| `Presentation/Scenes/ProgressPhotos/TimeLapsePlayerView.swift` | Time-lapse viewer |
| `Presentation/Scenes/ProgressPhotos/ProgressPhotosViewModel.swift` | ViewModel |
### Files to Modify
| File | Change |
|------|--------|
| `PlantGuideModel.xcdatamodeld` | Add `ProgressPhotoMO` entity |
| `PlantDetailView.swift` | Add "Progress Photos" section |
| `NotificationService.swift` | Add photo reminder notification type |
| `DIContainer.swift` | Register photo repositories and use cases |
### Core Data Entity
```
ProgressPhotoMO
├── id: UUID
├── plantID: UUID
├── imageData: Binary Data (External Storage)
├── thumbnailData: Binary Data
├── dateTaken: Date
├── notes: String?
└── plant: PlantMO (relationship)
```
### Time-Lapse Player
```
┌─────────────────────────────────┐
│ [Photo Display] │
│ │
│ ◀ ▶ Jan 15, 2026 │
│ │
│ Speed: [━━━━●━━━━━] 0.3s/frame │
│ │
│ [ Play ] │
└─────────────────────────────────┘
```
### Photo Reminder Options
- Weekly (same day each week)
- Bi-weekly
- Monthly (same date each month)
- Off
### Storage Strategy
- Full resolution stored as `Binary Data` with "Allows External Storage" enabled
- Thumbnail (200x200) stored inline for fast gallery loading
- CloudKit syncs via `CKAsset` automatically
### Risks & Mitigations
| Risk | Mitigation |
|------|------------|
| Storage bloat | Compress to HEIC, limit resolution to 2048px max dimension |
| CloudKit photo sync slow | Show sync indicator, allow offline viewing of local photos |
| Gallery performance | Load thumbnails only, full image on tap |
### Tests
- Unit: Photo saved with correct metadata
- Unit: Thumbnail generated at correct size
- UI: Time-lapse plays at correct speed
- Integration: Photo syncs to CloudKit
---
## Red Team Check
### Potential Issues Across All Phases
| Issue | Phase | Severity | Mitigation |
|-------|-------|----------|------------|
| CloudKit container misconfigured | 2 | Critical | Test on real device early, not just simulator |
| Color tokens not adopted everywhere | 1 | Medium | Add SwiftLint rule or grep check |
| Room deletion orphans data | 3 | High | Cascade to "Other" room, never delete "Other" |
| Animation performance on old devices | 4 | Medium | Test on iPhone XR/XS, use `interactiveSpring` |
| Snooze notifications don't update | 5 | High | Wrap in transaction: cancel + reschedule |
| Today view empty state confusing | 6 | Low | Design clear empty states early |
| Batch action logging wrong | 7 | Medium | Unit test verifies individual records |
| Photo storage exceeds iCloud quota | 8 | Medium | Warn user when approaching limit |
### Rollback Strategy
Each phase is independently deployable. If issues arise:
1. **Feature flags**: Wrap new features in `@AppStorage("feature_X_enabled")` flags
2. **Database migrations**: All Core Data changes are additive (new entities/attributes), never destructive
3. **CloudKit schema**: Once deployed to production, schema can only be extended, not modified
---
## Totals
| Phase | Feature | New Files | Modified Files | Effort |
|-------|---------|-----------|----------------|--------|
| 1 | Dark Mode | 3 | ~15 | S |
| 2 | CloudKit | 0 | 4 | M |
| 3 | Rooms | 8 | 7 | M |
| 4 | Animations | 6 | 5 | M |
| 5 | Snoozing | 1 | 5 | S |
| 6 | Today View | 5 | 2 | M |
| 7 | Batch Actions | 2 | 4 | S |
| 8 | Progress Photos | 9 | 4 | L |
**Total: ~34 new files, ~30 modified files**