diff --git a/CLAUDE.md b/CLAUDE.md index e8f34dc..a2f3ec6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -124,6 +124,391 @@ Task { } ``` +### 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`.