Fix residence auto-update, widget theming, and document patterns

- Fix residence detail not updating after edit:
  - DataManager.updateResidence() now updates both _residences and _myResidences
  - ResidenceViewModel auto-updates selectedResidence when data changes
  - No pull-to-refresh needed after editing

- Add widget theme support:
  - Widgets now use user's selected theme via App Group UserDefaults
  - ThemeManager has simplified version for widget extension context
  - Added WIDGET_EXTENSION compiler flag to CaseraExtension target

- Redesign widget views with organic aesthetic:
  - Updated FreeWidgetView, SmallWidgetView, MediumWidgetView, LargeWidgetView
  - Created OrganicTaskRowView, OrganicStatsView, OrganicStatPillWidget

- Document patterns in CLAUDE.md:
  - Added Mutation & Auto-Update Pattern section
  - Added iOS Shared Components documentation
  - Documented reusable buttons, forms, empty states, cards, modifiers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-17 22:58:55 -06:00
parent 7d76393e40
commit b39d37a6e8
8 changed files with 719 additions and 254 deletions

322
CLAUDE.md
View File

@@ -146,6 +146,328 @@ Task {
}
```
### Mutation & Auto-Update Pattern
**CRITICAL: When implementing CRUD operations, follow this pattern to ensure UI updates automatically without requiring pull-to-refresh.**
#### Kotlin DataManager Update Methods
When updating a single item, ensure ALL related caches are updated:
```kotlin
// ✅ CORRECT: Update all caches that contain the item
fun updateResidence(residence: Residence) {
// Update primary list
_residences.value = _residences.value.map {
if (it.id == residence.id) residence else it
}
// Also update related caches (myResidences is checked by getResidence)
_myResidences.value?.let { myRes ->
val updatedResidences = myRes.residences.map {
if (it.id == residence.id) residence else it
}
_myResidences.value = myRes.copy(residences = updatedResidences)
}
persistToDisk()
}
// ❌ WRONG: Only updating one cache causes stale data
fun updateResidence(residence: Residence) {
_residences.value = _residences.value.map {
if (it.id == residence.id) residence else it
}
// Missing: _myResidences update - getResidence may return stale data!
}
```
#### iOS ViewModel: Auto-Update Selected Items
When a detail view displays a `selectedItem`, the ViewModel must auto-update it when DataManager data changes:
```swift
// CORRECT: Auto-update selectedResidence when data changes
init() {
// Observe residences list
DataManagerObservable.shared.$residences
.receive(on: DispatchQueue.main)
.sink { [weak self] residences in
self?.residences = residences
// Auto-update selectedResidence if it exists in the updated list
if let currentSelected = self?.selectedResidence,
let updatedResidence = residences.first(where: { $0.id == currentSelected.id }) {
self?.selectedResidence = updatedResidence
}
}
.store(in: &cancellables)
// Also observe myResidences (another source of residence data)
DataManagerObservable.shared.$myResidences
.receive(on: DispatchQueue.main)
.sink { [weak self] myResidences in
self?.myResidences = myResidences
// Auto-update selectedResidence here too
if let currentSelected = self?.selectedResidence,
let updatedResidence = myResidences?.residences.first(where: { $0.id == currentSelected.id }) {
self?.selectedResidence = updatedResidence
}
}
.store(in: &cancellables)
}
// WRONG: Only storing list data, selectedResidence becomes stale
init() {
DataManagerObservable.shared.$residences
.sink { [weak self] residences in
self?.residences = residences
// Missing: selectedResidence auto-update!
}
.store(in: &cancellables)
}
```
#### Complete Data Flow for Mutations
```
1. User edits item in FormView (has its own ViewModel instance)
2. FormView.viewModel calls APILayer.updateItem()
3. APILayer calls API, on success:
- DataManager.updateItem() updates ALL relevant caches
- Returns updated item
4. DataManager StateFlows emit new values
5. DataManagerObservable picks up changes, publishes to @Published
6. ALL ViewModels observing that data receive updates via Combine
7. Each ViewModel's sink checks if selectedItem matches and updates it
8. SwiftUI re-renders automatically - NO pull-to-refresh needed
```
#### Checklist for New CRUD Features
- [ ] Kotlin `DataManager.updateX()` updates ALL caches containing that data type
- [ ] iOS ViewModel observes ALL relevant `DataManagerObservable` publishers
- [ ] iOS ViewModel auto-updates `selectedX` in each Combine sink
- [ ] No manual refresh calls needed after mutations (architecture handles it)
### iOS Shared Components (`iosApp/iosApp/Shared/`)
**CRITICAL: Always check the Shared folder for reusable components before creating new ones.** This folder contains standardized UI components, view modifiers, and utilities that ensure consistency across the app.
#### Directory Structure
```
iosApp/iosApp/Shared/
├── Components/
│ ├── FormComponents.swift # Form headers, sections, text fields
│ ├── ButtonStyles.swift # Primary, secondary, destructive buttons
│ └── SharedEmptyStateView.swift # Empty state views
├── Modifiers/
│ └── CardModifiers.swift # Card styling modifiers
├── Extensions/
│ ├── ViewExtensions.swift # Form styling, loading overlays
│ ├── StringExtensions.swift # String utilities
│ ├── DoubleExtensions.swift # Number formatting
│ └── DateExtensions.swift # Date formatting
└── Utilities/
├── ValidationHelpers.swift # Form validation
└── SharedErrorMessageParser.swift
```
#### Reusable Button Components
```swift
// Primary filled button (main actions)
PrimaryButton(title: "Save", icon: "checkmark", isLoading: isLoading) {
saveAction()
}
// Secondary outlined button
SecondaryButton(title: "Cancel", icon: "xmark") {
cancelAction()
}
// Destructive button (delete, remove)
DestructiveButton(title: "Delete", icon: "trash") {
deleteAction()
}
// Text-only button
TextButton(title: "Learn More", icon: "arrow.right") {
navigateAction()
}
// Compact button for cards/rows
CompactButton(title: "Edit", icon: "pencil", color: .appPrimary, isFilled: false) {
editAction()
}
// Organic button with gradient (premium feel)
OrganicPrimaryButton(title: "Continue", isLoading: isLoading) {
continueAction()
}
```
#### Reusable Form Components
```swift
// Form header with icon
FormHeader(
icon: "house.fill",
title: "Add Property",
subtitle: "Enter your property details"
)
// Organic form header (radial gradient style)
OrganicFormHeader(
icon: "person.fill",
title: "Create Account",
subtitle: "Join Casera today"
)
// Form section with icon header
IconFormSection(icon: "info.circle", title: "Details", footer: "Optional info") {
TextField("Name", text: $name)
}
// Error display section
if let error = errorMessage {
ErrorSection(message: error)
}
// Success message section
SuccessSection(message: "Changes saved successfully")
// Form action button (submit)
FormActionButton(title: "Submit", isLoading: isSubmitting) {
submitForm()
}
// Text field with icon
IconTextField(
icon: "envelope",
placeholder: "Email",
text: $email,
keyboardType: .emailAddress
)
// Secure text field with visibility toggle
SecureIconTextField(
icon: "lock",
placeholder: "Password",
text: $password,
isVisible: $showPassword
)
// Field label with optional required indicator
FieldLabel(text: "Username", isRequired: true)
// Field error message
FieldError(message: "This field is required")
```
#### Reusable Empty State Views
```swift
// Standard empty state
StandardEmptyStateView(
icon: "tray",
title: "No Items",
subtitle: "Get started by adding your first item",
actionLabel: "Add Item",
action: { showAddSheet = true }
)
// Organic empty state (matches app design)
OrganicEmptyState(
icon: "house",
title: "No Properties",
subtitle: "Add your first property to get started",
actionLabel: "Add Property",
action: { showAddSheet = true },
blobVariation: 1
)
// Simple list empty state
ListEmptyState(icon: "doc.text", message: "No documents found")
```
#### Reusable Card Modifiers
```swift
// Standard card styling
VStack { content }
.standardCard() // Default padding, background, shadow
// Compact card (smaller padding)
HStack { content }
.compactCard()
// Organic card (matches design system)
VStack { content }
.organicCardStyle(showBlob: true, blobVariation: 0)
// List row card
ForEach(items) { item in
ItemRow(item: item)
.listRowCard()
}
// Metadata pill (tags, badges)
Text("Active")
.metadataPill()
```
#### Reusable View Extensions
```swift
// Standard form styling (ALWAYS use on Form views)
Form {
// content
}
.standardFormStyle() // Applies .listStyle(.plain), .scrollContentBackground(.hidden)
// Section backgrounds
Section { content }
.sectionBackground() // Uses Color.appBackgroundSecondary
Section { headerContent }
.headerSectionBackground() // Clear background for headers
// Loading overlay
content
.loadingOverlay(isLoading: isLoading, message: "Saving...")
// Conditional modifiers
content
.if(condition) { view in
view.opacity(0.5)
}
// Dismiss keyboard on tap
ScrollView { content }
.dismissKeyboardOnTap()
// Standard loading view
StandardLoadingView(message: "Loading...")
```
#### When to Create New vs Reuse
**Reuse existing components when:**
- Building forms (use `FormComponents.swift`)
- Adding buttons (use `ButtonStyles.swift`)
- Showing empty states (use `SharedEmptyStateView.swift`)
- Styling cards (use `CardModifiers.swift`)
- Styling forms (use `ViewExtensions.swift`)
**Create new components when:**
- The pattern will be used 3+ times
- It doesn't fit existing components
- It's domain-specific (put in feature folder, not Shared)
**Add to Shared folder when:**
- Component is generic and reusable across features
- It follows existing design patterns
- It doesn't depend on specific business logic
### iOS Design System
**CRITICAL**: Always use the custom design system colors defined in `iosApp/iosApp/Design/DesignSystem.swift` and Xcode Asset Catalog. Never use system colors directly.