# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

**⚠️ Important:** This is the KMM mobile client repository. For full-stack documentation covering both the mobile app and backend API, see the root CLAUDE.md at `../CLAUDE.md`.

## Important Guidelines

**⚠️ DO NOT auto-commit code changes.** Always ask the user before committing. Only create commits when the user explicitly requests it with commands like "commit this work" or "create a commit".

## Project Overview

HoneyDue (honeyDue) is a Kotlin Multiplatform Mobile (KMM) property management application with shared business logic and platform-specific UI implementations. The backend is a Go REST API with PostgreSQL (located in the sibling `honeyDueAPI-go` directory).

**Tech Stack:**
- **Shared (Kotlin)**: Compose Multiplatform for Android, networking layer, ViewModels, models
- **iOS**: SwiftUI with Kotlin shared layer integration via SKIE
- **Backend**: Go REST API with PostgreSQL (separate directory at `../honeyDueAPI-go`)

## Build Commands

### Android
```bash
# Build debug APK
./gradlew :composeApp:assembleDebug

# Build release APK
./gradlew :composeApp:assembleRelease

# Run on connected device/emulator
./gradlew :composeApp:installDebug
```

### iOS
```bash
# Build from command line (use Xcode for best experience)
xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17' build

# Or open in Xcode
open iosApp/iosApp.xcodeproj
```

### Desktop (JVM)
```bash
./gradlew :composeApp:run
```

### Web
```bash
# Wasm target (modern browsers)
./gradlew :composeApp:wasmJsBrowserDevelopmentRun

# JS target (older browser support)
./gradlew :composeApp:jsBrowserDevelopmentRun
```

## Architecture

### Shared Kotlin Layer (`composeApp/src/commonMain/kotlin/com/example/honeydue/`)

**Core Components:**

1. **DataManager** (`data/DataManager.kt`) - **Single Source of Truth**
   - Unified cache for ALL app data (auth, residences, tasks, lookups, etc.)
   - All data is exposed via `StateFlow` for reactive UI updates
   - Automatic cache timeout validation (1 hour default)
   - Persists data to disk for offline access
   - Platform-specific initialization (TokenManager, ThemeStorage, PersistenceManager)
   - O(1) lookup helpers: `getTaskPriority(id)`, `getTaskCategory(id)`, etc.

2. **APILayer** (`network/APILayer.kt`) - **Single Entry Point for Network Calls**
   - Every API response immediately updates DataManager
   - All screens observe DataManager StateFlows directly
   - Handles cache-first reads with `forceRefresh` parameter
   - ETag-based conditional fetching for lookups (304 Not Modified support)
   - Guards against concurrent initialization/prefetch calls
   - Returns `ApiResult<T>` (Success/Error/Loading/Idle states)

3. **API Clients** (`network/*Api.kt`)
   - Domain-specific API clients: `ResidenceApi`, `TaskApi`, `ContractorApi`, etc.
   - Low-level HTTP calls using Ktor
   - Error parsing and response handling

4. **PersistenceManager** (`data/PersistenceManager.kt`)
   - Platform-specific disk persistence (expect/actual pattern)
   - Stores serialized JSON for offline access
   - Loads cached data on app startup

5. **ViewModels** (`viewmodel/`)
   - Thin wrappers that call APILayer methods
   - Expose loading/error states for UI feedback
   - ViewModels: `ResidenceViewModel`, `TaskViewModel`, `AuthViewModel`, etc.

**Data Flow:**
```
User Action → ViewModel → APILayer → API Client → Server Response
                                          ↓
                              DataManager Updated (cache + disk)
                                          ↓
                              All Screens React (StateFlow observers)
```

**Cache Architecture:**
```kotlin
// DataManager exposes StateFlows that UI observes directly
DataManager.residences: StateFlow<List<Residence>>
DataManager.myResidences: StateFlow<MyResidencesResponse?>
DataManager.allTasks: StateFlow<TaskColumnsResponse?>
DataManager.taskCategories: StateFlow<List<TaskCategory>>

// Cache validation (1 hour timeout)
DataManager.isCacheValid(DataManager.residencesCacheTime)

// O(1) lookups for IDs
DataManager.getTaskPriority(task.priorityId)  // Returns TaskPriority?
DataManager.getTaskCategory(task.categoryId)  // Returns TaskCategory?
```

### iOS Layer (`iosApp/iosApp/`)

**Integration Pattern:**
- SwiftUI views wrap Kotlin ViewModels via `@StateObject`
- iOS-specific ViewModels (Swift) wrap shared Kotlin ViewModels
- Pattern: `@Published var data` in Swift observes Kotlin `StateFlow` via async iteration
- Navigation uses SwiftUI `NavigationStack` with sheets for modals

**Key iOS Files:**
- `MainTabView.swift`: Tab-based navigation
- `*ViewModel.swift` (Swift): Wraps shared Kotlin ViewModels, exposes `@Published` properties
- `*View.swift`: SwiftUI screens
- Directory structure mirrors feature organization (Residence/, Task/, Contractor/, etc.)

**iOS ↔ Kotlin Bridge:**
```swift
// Swift ViewModel wraps Kotlin ViewModel
@StateObject private var viewModel = ResidenceViewModel() // Swift wrapper
// Inside: let sharedViewModel: ComposeApp.ResidenceViewModel // Kotlin

// Observe Kotlin StateFlow
Task {
    for await state in sharedViewModel.residencesState {
        await MainActor.run {
            self.residences = (state as? ApiResultSuccess)?.data
        }
    }
}
```

### 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 honeyDue 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.

#### Color Palette

The app uses a 5-color semantic design system:

```swift
// Primary Colors
Color.appPrimary          // #07A0C3 (BlueGreen) - Primary actions, important icons
Color.appSecondary        // #0055A5 (Cerulean) - Secondary actions
Color.appAccent           // #F5A623 (BrightAmber) - Highlights, notifications, accents

// Status Colors
Color.appError            // #DD1C1A (PrimaryScarlet) - Errors, destructive actions

// Background Colors
Color.appBackgroundPrimary    // #FFF1D0 (cream) light / #0A1929 dark - Screen backgrounds
Color.appBackgroundSecondary  // Blue-gray - Cards, list rows, elevated surfaces

// Text Colors
Color.appTextPrimary      // Primary text (dark mode aware)
Color.appTextSecondary    // Secondary text (less emphasis)
Color.appTextOnPrimary    // Text on primary colored backgrounds (white)
```

**Color Usage Guidelines:**
- **Buttons**: Primary buttons use `Color.appPrimary`, destructive buttons use `Color.appError`
- **Icons**: Use `Color.appPrimary` for main actions, `Color.appAccent` for secondary/info icons
- **Cards**: Always use `Color.appBackgroundSecondary` for card backgrounds
- **Screens**: Always use `Color.appBackgroundPrimary` for main view backgrounds
- **Text**: Use `Color.appTextPrimary` for body text, `Color.appTextSecondary` for captions/subtitles

#### Creating New Views

**Standard Form/List View Pattern:**

```swift
import SwiftUI
import ComposeApp

struct MyNewView: View {
    @Environment(\.dismiss) private var dismiss
    @StateObject private var viewModel = MyViewModel()
    @FocusState private var focusedField: Field?

    enum Field {
        case fieldOne, fieldTwo
    }

    var body: some View {
        NavigationStack {
            Form {
                // Header Section (optional, with clear background)
                Section {
                    VStack(spacing: 16) {
                        Image(systemName: "icon.name")
                            .font(.system(size: 60))
                            .foregroundStyle(Color.appPrimary.gradient)

                        Text("View Title")
                            .font(.title2)
                            .fontWeight(.bold)
                            .foregroundColor(Color.appTextPrimary)

                        Text("Subtitle description")
                            .font(.subheadline)
                            .foregroundColor(Color.appTextSecondary)
                    }
                    .frame(maxWidth: .infinity)
                    .padding(.vertical)
                }
                .listRowBackground(Color.clear)

                // Data Section
                Section {
                    TextField("Field One", text: $viewModel.fieldOne)
                        .focused($focusedField, equals: .fieldOne)

                    TextField("Field Two", text: $viewModel.fieldTwo)
                        .focused($focusedField, equals: .fieldTwo)
                } header: {
                    Text("Section Header")
                } footer: {
                    Text("Helper text here")
                }
                .listRowBackground(Color.appBackgroundSecondary)

                // Error Section (conditional)
                if let error = viewModel.errorMessage {
                    Section {
                        HStack {
                            Image(systemName: "exclamationmark.triangle.fill")
                                .foregroundColor(Color.appError)
                            Text(error)
                                .foregroundColor(Color.appError)
                                .font(.subheadline)
                        }
                    }
                    .listRowBackground(Color.appBackgroundSecondary)
                }

                // Action Button Section
                Section {
                    Button(action: { /* action */ }) {
                        HStack {
                            Spacer()
                            if viewModel.isLoading {
                                ProgressView()
                            } else {
                                Text("Submit")
                                    .fontWeight(.semibold)
                            }
                            Spacer()
                        }
                    }
                    .disabled(viewModel.isLoading)
                }
                .listRowBackground(Color.appBackgroundSecondary)
            }
            .listStyle(.plain)
            .scrollContentBackground(.hidden)
            .background(Color.appBackgroundPrimary)
            .navigationTitle("Title")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
            }
        }
    }
}
```

**CRITICAL Form/List Styling Rules:**

1. **Always add these three modifiers to Form/List:**
   ```swift
   .listStyle(.plain)
   .scrollContentBackground(.hidden)
   .background(Color.appBackgroundPrimary)
   ```

2. **Always add `.listRowBackground()` to EVERY Section:**
   ```swift
   Section {
       // content
   }
   .listRowBackground(Color.appBackgroundSecondary)  // ← REQUIRED
   ```

3. **Exception for header sections:** Use `.listRowBackground(Color.clear)` for decorative headers

#### Creating Custom Cards

**Standard Card Pattern:**

```swift
struct MyCard: View {
    let item: MyModel

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            // Header
            HStack {
                Image(systemName: "icon.name")
                    .font(.title2)
                    .foregroundColor(Color.appPrimary)

                Text(item.title)
                    .font(.headline)
                    .foregroundColor(Color.appTextPrimary)

                Spacer()

                // Badge or status indicator
                Text("Status")
                    .font(.caption)
                    .foregroundColor(Color.appTextOnPrimary)
                    .padding(.horizontal, 8)
                    .padding(.vertical, 4)
                    .background(Color.appPrimary)
                    .clipShape(Capsule())
            }

            // Content
            Text(item.description)
                .font(.subheadline)
                .foregroundColor(Color.appTextSecondary)
                .lineLimit(2)

            // Footer
            HStack {
                Label("Info", systemImage: "info.circle")
                    .font(.caption)
                    .foregroundColor(Color.appTextSecondary)

                Spacer()

                Image(systemName: "chevron.right")
                    .font(.caption)
                    .foregroundColor(Color.appTextSecondary)
            }
        }
        .padding()
        .background(Color.appBackgroundSecondary)
        .clipShape(RoundedRectangle(cornerRadius: 12))
        .shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1)
    }
}
```

**Card Design Guidelines:**
- Background: `Color.appBackgroundSecondary`
- Corner radius: 12pt
- Padding: 16pt (standard) or 12pt (compact)
- Shadow: `Color.black.opacity(0.1), radius: 2, x: 0, y: 1`
- Use `VStack` for vertical layout, `HStack` for horizontal

#### Creating Buttons

**Primary Button:**
```swift
Button(action: { /* action */ }) {
    Text("Primary Action")
        .fontWeight(.semibold)
        .frame(maxWidth: .infinity)
        .foregroundColor(Color.appTextOnPrimary)
        .padding()
        .background(Color.appPrimary)
        .clipShape(RoundedRectangle(cornerRadius: 8))
}
```

**Destructive Button:**
```swift
Button(action: { /* action */ }) {
    Label("Delete", systemImage: "trash")
        .foregroundColor(Color.appError)
}
```

**Secondary Button (bordered):**
```swift
Button(action: { /* action */ }) {
    Text("Secondary")
        .foregroundColor(Color.appPrimary)
}
.buttonStyle(.bordered)
```

#### Icons and SF Symbols

**Icon Coloring:**
- Primary actions: `Color.appPrimary` (e.g., add, edit)
- Secondary info: `Color.appAccent` (e.g., info, notification)
- Destructive: `Color.appError` (e.g., delete, warning)
- Neutral: `Color.appTextSecondary` (e.g., chevrons, decorative)

**Common Icon Patterns:**
```swift
// Large decorative icon
Image(systemName: "house.fill")
    .font(.system(size: 60))
    .foregroundStyle(Color.appPrimary.gradient)

// Inline icon with label
Label("Title", systemImage: "folder")
    .foregroundColor(Color.appPrimary)

// Status indicator icon
Image(systemName: "checkmark.circle.fill")
    .foregroundColor(Color.appPrimary)
```

#### Spacing and Layout

Use constants from `DesignSystem.swift`:

```swift
// Standard spacing
AppSpacing.xs      // 4pt
AppSpacing.sm      // 8pt
AppSpacing.md      // 12pt
AppSpacing.lg      // 16pt
AppSpacing.xl      // 24pt

// Example usage
VStack(spacing: AppSpacing.md) {
    // content
}
```

#### Adding New Colors to Asset Catalog

If you need to add a new semantic color:

1. Open `iosApp/iosApp/Assets.xcassets/Colors/Semantic/`
2. Create new `.colorset` folder
3. Add `Contents.json`:
```json
{
  "colors" : [
    {
      "color" : {
        "color-space" : "srgb",
        "components" : {
          "alpha" : "1.000",
          "blue" : "0xHH",
          "green" : "0xHH",
          "red" : "0xHH"
        }
      },
      "idiom" : "universal"
    },
    {
      "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ],
      "color" : {
        "color-space" : "srgb",
        "components" : {
          "alpha" : "1.000",
          "blue" : "0xHH",
          "green" : "0xHH",
          "red" : "0xHH"
        }
      },
      "idiom" : "universal"
    }
  ],
  "info" : { "author" : "xcode", "version" : 1 }
}
```

4. Add extension in `DesignSystem.swift`:
```swift
extension Color {
    static let appNewColor = Color("NewColor")
}
```

#### View Modifiers and Helpers

**Error Handling Modifier:**
```swift
.handleErrors(
    error: viewModel.errorMessage,
    onRetry: { viewModel.retryAction() }
)
```

**Loading State:**
```swift
if viewModel.isLoading {
    ProgressView()
        .tint(Color.appPrimary)
} else {
    // content
}
```

**Empty States:**
```swift
if items.isEmpty {
    VStack(spacing: 16) {
        Image(systemName: "tray")
            .font(.system(size: 60))
            .foregroundColor(Color.appTextSecondary.opacity(0.5))

        Text("No Items")
            .font(.headline)
            .foregroundColor(Color.appTextPrimary)

        Text("Get started by adding your first item")
            .font(.subheadline)
            .foregroundColor(Color.appTextSecondary)
            .multilineTextAlignment(.center)
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
}
```

### Android Layer

Android uses Compose UI directly from `composeApp` with shared ViewModels. Navigation via Jetpack Compose Navigation in `App.kt`.

### Android Design System

**CRITICAL**: Always use the theme-aware design system components and colors. Never use hardcoded colors or spacing values.

#### Theme System

The app uses a comprehensive theming system with 11 themes matching iOS:
- **Default** (vibrant iOS system colors)
- **Teal**, **Ocean**, **Forest**, **Sunset**
- **Monochrome**, **Lavender**, **Crimson**, **Midnight**, **Desert**, **Mint**

**Theme Files:**
- `ui/theme/ThemeColors.kt` - All 11 themes with light/dark mode colors
- `ui/theme/ThemeManager.kt` - Singleton for dynamic theme switching with persistence
- `ui/theme/Spacing.kt` - Standardized spacing constants
- `ui/theme/Theme.kt` - Material3 theme integration

**Theme Usage:**
```kotlin
@Composable
fun App() {
    val currentTheme by remember { derivedStateOf { ThemeManager.currentTheme } }

    HoneyDueTheme(themeColors = currentTheme) {
        // App content
    }
}
```

**Changing Themes:**
```kotlin
// In ProfileScreen or settings
ThemeManager.setTheme("ocean")  // By ID
// or
ThemeManager.setTheme(AppThemes.Ocean)  // By object
```

**Theme Persistence:**
Themes are automatically persisted using `ThemeStorage` (SharedPreferences on Android, UserDefaults on iOS). Initialize in MainActivity:
```kotlin
ThemeStorage.initialize(ThemeStorageManager.getInstance(applicationContext))
ThemeManager.initialize()  // Loads saved theme
```

#### Color System

**ALWAYS use MaterialTheme.colorScheme instead of hardcoded colors:**

```kotlin
// ✅ CORRECT
Text(
    text = "Hello",
    color = MaterialTheme.colorScheme.onBackground
)

Card(
    colors = CardDefaults.cardColors(
        containerColor = MaterialTheme.colorScheme.backgroundSecondary
    )
)

// ❌ WRONG
Text(
    text = "Hello",
    color = Color(0xFF000000)  // Never hardcode colors!
)
```

**Available Material3 ColorScheme Properties:**
- `primary`, `onPrimary` - Primary brand color and text on it
- `secondary`, `onSecondary` - Secondary brand color
- `error`, `onError` - Error states
- `background`, `onBackground` - Screen backgrounds
- `surface`, `onSurface` - Card/surface backgrounds
- `surfaceVariant`, `onSurfaceVariant` - Alternative surface colors
- **Custom extensions:**
  - `backgroundSecondary` - For cards and elevated surfaces
  - `textPrimary`, `textSecondary` - Semantic text colors

#### Spacing System

**ALWAYS use AppSpacing constants instead of hardcoded dp values:**

```kotlin
// ✅ CORRECT
Column(
    verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
) {
    Box(modifier = Modifier.padding(AppSpacing.lg))
}

// ❌ WRONG
Column(
    verticalArrangement = Arrangement.spacedBy(12.dp)  // Never hardcode spacing!
)
```

**Available Spacing:**
```kotlin
AppSpacing.xs   // 4.dp - Minimal spacing
AppSpacing.sm   // 8.dp - Small spacing
AppSpacing.md   // 12.dp - Medium spacing (default)
AppSpacing.lg   // 16.dp - Large spacing
AppSpacing.xl   // 24.dp - Extra large spacing
```

**Available Radius:**
```kotlin
AppRadius.xs    // 4.dp
AppRadius.sm    // 8.dp
AppRadius.md    // 12.dp - Standard card radius
AppRadius.lg    // 16.dp
AppRadius.xl    // 20.dp
AppRadius.xxl   // 24.dp
```

#### Standard Components

**Use the provided standard components for consistency:**

**1. StandardCard - Primary card component:**
```kotlin
StandardCard(
    modifier = Modifier.fillMaxWidth(),
    contentPadding = AppSpacing.lg  // Default
) {
    Text("Card content")
    // More content...
}

// With custom background
StandardCard(
    backgroundColor = MaterialTheme.colorScheme.primaryContainer
) {
    Text("Highlighted card")
}
```

**2. CompactCard - Smaller card variant:**
```kotlin
CompactCard {
    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Text("Title")
        Icon(Icons.Default.ChevronRight, null)
    }
}
```

**3. FormTextField - Standardized input field:**
```kotlin
var text by remember { mutableStateOf("") }
var error by remember { mutableStateOf<String?>(null) }

FormTextField(
    value = text,
    onValueChange = { text = it },
    label = "Property Name",
    placeholder = "Enter name",
    leadingIcon = Icons.Default.Home,
    error = error,
    helperText = "This will be displayed on your dashboard",
    keyboardType = KeyboardType.Text
)
```

**4. FormSection - Group related form fields:**
```kotlin
FormSection(
    header = "Property Details",
    footer = "Enter the basic information about your property"
) {
    FormTextField(value = name, onValueChange = { name = it }, label = "Name")
    FormTextField(value = address, onValueChange = { address = it }, label = "Address")
}
```

**5. StandardEmptyState - Consistent empty states:**
```kotlin
if (items.isEmpty()) {
    StandardEmptyState(
        icon = Icons.Default.Home,
        title = "No Properties",
        subtitle = "Add your first property to get started",
        actionLabel = "Add Property",
        onAction = { navigateToAddProperty() }
    )
}
```

#### Screen Patterns

**Standard Screen Structure:**
```kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScreen(
    onNavigateBack: () -> Unit,
    viewModel: MyViewModel = viewModel { MyViewModel() }
) {
    val state by viewModel.state.collectAsState()

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Title", fontWeight = FontWeight.SemiBold) },
                navigationIcon = {
                    IconButton(onClick = onNavigateBack) {
                        Icon(Icons.Default.ArrowBack, "Back")
                    }
                },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.surface
                )
            )
        }
    ) { paddingValues ->
        // Content with proper padding
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
                .padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md),
            verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
        ) {
            when (state) {
                is ApiResult.Success -> {
                    // Content
                }
                is ApiResult.Loading -> {
                    CircularProgressIndicator()
                }
                is ApiResult.Error -> {
                    ErrorCard(message = state.message)
                }
            }
        }
    }
}
```

**List Screen with Pull-to-Refresh:**
```kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ListScreen() {
    var isRefreshing by remember { mutableStateOf(false) }
    val items by viewModel.items.collectAsState()

    PullToRefreshBox(
        isRefreshing = isRefreshing,
        onRefresh = {
            isRefreshing = true
            viewModel.loadItems(forceRefresh = true)
        }
    ) {
        LazyColumn {
            items(items) { item ->
                StandardCard(
                    modifier = Modifier
                        .fillMaxWidth()
                        .clickable { onClick(item) }
                ) {
                    // Item content
                }
            }
        }
    }
}
```

#### Button Patterns

```kotlin
// Primary Action Button
Button(
    onClick = { /* action */ },
    modifier = Modifier
        .fillMaxWidth()
        .height(56.dp),
    shape = RoundedCornerShape(AppRadius.md)
) {
    Icon(Icons.Default.Save, null)
    Spacer(Modifier.width(AppSpacing.sm))
    Text("Save Changes", fontWeight = FontWeight.SemiBold)
}

// Destructive Button
Button(
    onClick = { /* action */ },
    colors = ButtonDefaults.buttonColors(
        containerColor = MaterialTheme.colorScheme.error
    )
) {
    Icon(Icons.Default.Delete, null)
    Text("Delete")
}

// Text Button
TextButton(onClick = { /* action */ }) {
    Text("Cancel")
}
```

#### Dialog Pattern

```kotlin
@Composable
fun ThemePickerDialog(
    currentTheme: ThemeColors,
    onThemeSelected: (ThemeColors) -> Unit,
    onDismiss: () -> Unit
) {
    Dialog(onDismissRequest = onDismiss) {
        Card(
            shape = RoundedCornerShape(AppRadius.lg),
            colors = CardDefaults.cardColors(
                containerColor = MaterialTheme.colorScheme.background
            )
        ) {
            Column(modifier = Modifier.padding(AppSpacing.xl)) {
                Text(
                    "Choose Theme",
                    style = MaterialTheme.typography.headlineSmall
                )
                // Content...
            }
        }
    }
}
```

#### Key Design Principles

1. **Always use theme-aware colors** from MaterialTheme.colorScheme
2. **Always use spacing constants** from AppSpacing/AppRadius
3. **Use standard components** (StandardCard, FormTextField, etc.) for consistency
4. **Follow Material3 guidelines** for component usage
5. **Support dynamic theming** - never assume a specific theme
6. **Test in both light and dark mode** - all themes support both

## Environment Configuration

**API Environment Toggle** (`composeApp/src/commonMain/kotlin/com/example/honeydue/network/ApiConfig.kt`):

```kotlin
val CURRENT_ENV = Environment.DEV  // or Environment.LOCAL
```

- `Environment.LOCAL`: Points to `http://10.0.2.2:8000/api` (Android emulator) or `http://127.0.0.1:8000/api` (iOS simulator)
- `Environment.DEV`: Points to `https://honeyDue.treytartt.com/api`

**Change this to switch between local Go backend and production server.**

## Common Development Patterns

### Adding a New API Endpoint

1. Add API call to appropriate `*Api.kt` class in `network/` (e.g., `TaskApi.kt`)
2. Add method to `APILayer.kt` that manages caching (if applicable)
3. Add method to relevant ViewModel that calls APILayer
4. Update UI to observe the new StateFlow

### Onboarding task suggestions (server-driven)

The First-Task onboarding screen is **fully server-driven** on both
platforms. There is no hardcoded catalog or client-side suggestion rules;
when the API fails the screen shows error + Retry + Skip.

**Data flow:**

```
"For You" tab  → APILayer.getTaskSuggestions(residenceId)
                 → GET /api/tasks/suggestions/?residence_id=X
                 → scored against 15 home-profile fields (incl. climate zone)

"Browse All" tab → APILayer.getTaskTemplatesGrouped()
                 → GET /api/tasks/templates/grouped/
                 → cached on DataManager.taskTemplatesGrouped (24h TTL)

Submit          → APILayer.bulkCreateTasks(BulkCreateTasksRequest)
                 → POST /api/tasks/bulk/
                 → single DB transaction, all-or-nothing
```

**Key files:**

- Shared ViewModel: `composeApp/.../viewmodel/OnboardingViewModel.kt`
  (`suggestionsState`, `templatesGroupedState`, `createTasks`)
- Android screen: `composeApp/.../ui/screens/onboarding/OnboardingFirstTaskContent.kt`
- iOS Swift wrapper: `iosApp/iosApp/Onboarding/OnboardingTasksViewModel.swift`
  (mirrors the Kotlin ViewModel but calls `APILayer.shared` directly in
  Swift rather than observing Kotlin StateFlows — matches the convention in
  `iosApp/iosApp/Task/TaskViewModel.swift`)
- iOS view: `iosApp/iosApp/Onboarding/OnboardingFirstTaskView.swift`
- Analytics: 5 shared event names in `AnalyticsEvents` (Kotlin) +
  `AnalyticsEvent` (Swift) — `onboarding_suggestions_loaded`,
  `onboarding_suggestion_accepted`, `onboarding_browse_template_accepted`,
  `onboarding_tasks_created`, `onboarding_task_step_skipped`.

**When selecting a template from either tab**, always populate
`TaskCreateRequest.templateId` with the backend `TaskTemplate.id` so the
created task carries the template backlink for reporting. Swift wraps the
id as `KotlinInt(int: template.id)`.

### Handling Platform-Specific Code

Use `expect/actual` pattern:

```kotlin
// commonMain
expect fun platformSpecificFunction(): String

// androidMain
actual fun platformSpecificFunction(): String = "Android"

// iosMain
actual fun platformSpecificFunction(): String = "iOS"
```

### Type Conversions for iOS

Kotlin types bridge to Swift with special wrappers:
- `Double` → `KotlinDouble` (use `KotlinDouble(double:)` constructor)
- `Int` → `KotlinInt` (use `KotlinInt(int:)` constructor)
- `String` stays `String`
- Optional types: Kotlin nullable (`Type?`) becomes Swift optional (`Type?`)

**Example iOS form submission:**
```swift
// TextField uses String binding
@State private var estimatedCost: String = ""

// Convert to KotlinDouble for API
estimatedCost: estimatedCost.isEmpty ? nil : KotlinDouble(double: Double(estimatedCost) ?? 0.0)
```

### Refreshing Lists After Mutations

**iOS Pattern:**
```swift
.sheet(isPresented: $showingAddForm) {
    AddFormView(
        isPresented: $showingAddForm,
        onSuccess: {
            viewModel.loadData(forceRefresh: true)
        }
    )
}
```

**Android Pattern:**
```kotlin
// Use savedStateHandle to pass refresh flag between screens
navController.previousBackStackEntry?.savedStateHandle?.set("refresh", true)
navController.popBackStack()

// In destination composable
val shouldRefresh = backStackEntry.savedStateHandle.get<Boolean>("refresh") ?: false
LaunchedEffect(shouldRefresh) {
    if (shouldRefresh) viewModel.loadData(forceRefresh = true)
}
```

## Testing

Currently tests are minimal. When adding tests:
- Android: Place in `composeApp/src/androidUnitTest/` or `composeApp/src/commonTest/`
- iOS: Use XCTest framework in Xcode project

## Key Dependencies

- Kotlin Multiplatform: 2.1.0
- Compose Multiplatform: 1.7.1
- Ktor Client: Network requests
- kotlinx.serialization: JSON serialization
- kotlinx.coroutines: Async operations
- SKIE: Kotlin ↔ Swift interop improvements

## Important Notes

### Committing Changes

When committing changes that span both iOS and Android, commit them together in the KMM repository. If backend changes are needed, commit separately in the `honeyDueAPI-go` repository.

### DataManager Initialization

**Critical**: DataManager must be initialized at app startup with platform-specific managers:

```kotlin
// In Application.onCreate() or equivalent
DataManager.initialize(
    tokenMgr = TokenManager(context),
    themeMgr = ThemeStorageManager(context),
    persistenceMgr = PersistenceManager(context)
)
```

After user login, call `APILayer.initializeLookups()` to populate DataManager with reference data. This uses ETag-based caching - if data hasn't changed, server returns 304 Not Modified.

```kotlin
// After successful login
val initResult = APILayer.initializeLookups()
if (initResult is ApiResult.Success) {
    // Now safe to navigate to main screen
    // Lookups are cached in DataManager and persisted to disk
}
```

Without this, dropdowns and pickers will be empty.

### iOS Build Issues

If iOS build fails with type mismatch errors:
1. Check that cost fields (estimatedCost, actualCost, purchasePrice) use `KotlinDouble`, not `String`
2. Verify preview/mock data matches current model signatures
3. Clean build folder in Xcode (Cmd+Shift+K) and rebuild

### Force Refresh Pattern

Always use `forceRefresh: true` when data should be fresh:
- After creating/updating/deleting items
- On pull-to-refresh gestures
- When explicitly requested by user

Without `forceRefresh`, APILayer returns cached data.

## Project Structure Summary

```
HoneyDueKMM/
├── composeApp/
│   └── src/
│       ├── commonMain/kotlin/com/example/honeydue/
│       │   ├── data/           # DataManager, PersistenceManager
│       │   ├── models/         # Shared data models (kotlinx.serialization)
│       │   ├── network/        # APILayer, *Api clients, ApiConfig
│       │   ├── storage/        # TokenManager, ThemeStorageManager
│       │   ├── util/           # DateUtils, helpers
│       │   ├── ui/             # Compose UI (Android)
│       │   │   ├── components/ # Reusable components
│       │   │   ├── screens/    # Screen composables
│       │   │   └── theme/      # Material theme, ThemeManager
│       │   ├── viewmodel/      # Shared ViewModels
│       │   └── App.kt          # Android navigation
│       ├── androidMain/        # Android-specific (TokenManager, etc.)
│       ├── iosMain/            # iOS-specific Kotlin code
│       └── commonTest/         # Shared tests
│
├── iosApp/iosApp/
│   ├── *ViewModel.swift        # Swift wrappers for Kotlin VMs
│   ├── *View.swift             # SwiftUI screens
│   ├── Components/             # Reusable SwiftUI components
│   ├── Design/                 # Design system (DesignSystem.swift, OrganicDesign.swift)
│   ├── Extensions/             # Swift extensions
│   ├── Helpers/                # Utility helpers (DateUtils, etc.)
│   ├── PushNotifications/      # APNs integration
│   └── [Feature]/              # Feature-grouped files
│       ├── Task/
│       ├── Residence/
│       ├── Contractor/
│       └── Documents/
│
└── gradle/                     # Gradle wrapper and configs
```

## Related Repositories

- **Backend API**: `../honeyDueAPI-go` - Go REST API with PostgreSQL
- **Documentation**: `../honeyDueAPI-go/docs` - Server configuration and API docs
