Cross-platform refinements to appearance/battery/charger editors, tabs and navigation, plus persistence, screenshot previews and CLAUDE.md docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
210 lines
13 KiB
Markdown
210 lines
13 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Two apps in this repo
|
|
|
|
- **iOS** (`Cable/`) — SwiftUI + SwiftData. **This document describes the iOS app unless stated otherwise.**
|
|
- **Android** (`android/`) — a native Kotlin/Jetpack Compose port that mirrors every iOS feature (package root `app.voltplan.cable`, Room persistence, same Aptabase analytics). See **`android/README.md`** for its architecture and build instructions before working on it.
|
|
|
|
Behavior and data shape are meant to stay in sync across both — when changing a user-facing feature on one platform, check the other. See [Export Options](#export-options) for one such cross-platform contract.
|
|
|
|
**Apply every instruction to both the iOS and Android versions unless explicitly told otherwise.** Any feature, fix, or change requested without naming a platform must land on both apps and stay behaviorally in sync.
|
|
|
|
## Build & Test Commands
|
|
|
|
Use the **Xcode MCP server** tools instead of `xcodebuild` CLI:
|
|
|
|
- **List windows first**: Always run `mcp__xcode__XcodeListWindows` to find the correct `tabIdentifier` — it changes depending on which Xcode windows are open.
|
|
- **Build**: `mcp__xcode__BuildProject`
|
|
- **Run all tests**: `mcp__xcode__RunAllTests`
|
|
- **Check errors**: `mcp__xcode__GetBuildLog` (severity: `error`) or `mcp__xcode__XcodeListNavigatorIssues`
|
|
|
|
No external dependencies beyond the Xcode toolchain.
|
|
|
|
## Architecture
|
|
|
|
**SwiftUI + SwiftData app** for sizing low-voltage electrical conductors (boats, RVs, off-grid).
|
|
|
|
### Data Model Hierarchy
|
|
|
|
`ElectricalSystem` is the top-level container. Child entities reference their parent via an optional `system: ElectricalSystem?` property:
|
|
- `SavedLoad` — individual electrical loads with wire sizing parameters
|
|
- `SavedBattery` — battery banks with chemistry-specific capacity rules
|
|
- `SavedCharger` — charging equipment specs
|
|
|
|
All are `@Model` classes persisted via SwiftData. The container is configured in `CableApp.swift` and injected into the SwiftUI environment.
|
|
|
|
### Key Layers
|
|
|
|
- **Calculation engine** (`Loads/ElectricalCalculations.swift`): Pure static functions for wire cross-section sizing, voltage drop, power loss, and fuse recommendations. Uses copper resistivity (0.017 Ω·mm²/m) with a 5% max voltage drop constraint. Supports both metric (mm²) and imperial (AWG) wire standards.
|
|
- **CableCalculator** (`Loads/CableCalculator.swift`): ObservableObject wrapper that bridges the calculation engine to SwiftUI views. Instantiated per-view as `@StateObject`, not globally injected.
|
|
- **App-wide state**: `UnitSystemSettings` (metric/imperial, persisted to UserDefaults) is injected as `@EnvironmentObject`.
|
|
|
|
### Navigation Flow
|
|
|
|
`SystemsView` (root list) → `LoadsView` (per-system TabView with 4 tabs: Overview, Components, Batteries, Chargers) → individual editor modals for each entity type.
|
|
|
|
### Feature Organization
|
|
|
|
Each feature has its own directory under `Cable/`: `Loads/`, `Systems/`, `Batteries/`, `Chargers/`, `Overview/`, `Shared/`. Models and views for each feature live together.
|
|
|
|
### SwiftData Query Pattern
|
|
|
|
`@Query` fetches all records globally, then a computed property filters by system relationship — SwiftData doesn't efficiently handle relationship-based predicates in `@Query`:
|
|
```swift
|
|
@Query(sort: \SavedLoad.timestamp, order: .reverse) private var allLoads: [SavedLoad]
|
|
private var savedLoads: [SavedLoad] { allLoads.filter { $0.system == system } }
|
|
```
|
|
|
|
### Unit Storage Convention
|
|
|
|
All values are stored in **metric** (meters, mm²). Conversion to imperial (feet, AWG) happens **at display time only**, never at storage time. `UnitSystemSettings.unitSystem` drives the display format.
|
|
|
|
## UI Patterns
|
|
|
|
### Editor Pattern (Battery, Charger, Load)
|
|
|
|
All editors follow the same architecture:
|
|
1. **Configuration struct** (e.g. `BatteryConfiguration`, `ChargerConfiguration`) as a transient draft — not an `@Model`. Has dual inits: one for new items, one from an existing `Saved*` model.
|
|
2. **Alert-based field editing**: Each numeric field gets its own `.alert()` with a `TextField`, bound via an `EditingField` enum. `onChange` updates the configuration live as the user types.
|
|
3. **Snap-to-values**: Common electrical values (12V, 24V, 48V, 230V, etc.) snap within a tolerance when the field is not actively being edited.
|
|
4. **Save flow**: `onSave: (Configuration) -> Void` callback to the parent view. `onDisappear` triggers save automatically. Parent calls `SystemComponentsPersistence` to persist.
|
|
5. **Appearance modal**: All editors include a `.sheet` presenting `ItemEditorView` for name/icon/color editing.
|
|
|
|
Navigation to editors uses `.navigationDestination(item: $draft)` where `$draft` is a `@State` optional Configuration.
|
|
|
|
### Shared UI Components
|
|
|
|
- **`ItemEditorView`** (`ItemEditorView.swift`): Shared appearance editor for name, icon (5-column SF Symbol grid), and color (6-column grid). Uses `AutoSelectTextField` for auto-focus. Accepts optional `additionalFields` builder.
|
|
- **`StatsHeaderContainer`** (`StatsHeaderContainer.swift`): Header card with scrollable metric pills. Uses `glassEffect` on iOS 26+, fallback with colored background + border.
|
|
- **`OnboardingInfoView`** (`Loads/OnboardingInfoView.swift`): Empty state with auto-rotating image carousel (8s interval). Configured via static factory methods: `.loads()`, `.battery()`, `.charger()`.
|
|
- **`LoadIconView`** (`Loads/LoadIconView.swift`): Component icon with remote URL support and `IconCache` (memory + file-based). Also exports `ComponentSummaryMetricView` and `ComponentMetricBadgeView`.
|
|
|
|
### List Styling Convention
|
|
|
|
All list views use consistent styling:
|
|
- `.listStyle(.plain)`, `.scrollContentBackground(.hidden)`, `.scrollIndicators(.hidden)`
|
|
- `.listRowSeparator(.hidden)`, `.listRowBackground(Color.clear)`
|
|
- Row cards: `RoundedRectangle(cornerRadius: 18, style: .continuous).fill(Color(.systemBackground))` with 16pt padding
|
|
- Headers via `.safeAreaInset(edge: .top)` containing `StatsHeaderContainer`
|
|
|
|
### Color Convention
|
|
|
|
`Color.componentColor(named:)` (in `LoadIconView.swift`) maps string names to SwiftUI colors (13 options: blue, green, orange, red, purple, yellow, pink, teal, indigo, mint, cyan, brown, gray). Used for all component icons. Badge backgrounds use `tint.opacity(0.12)`.
|
|
|
|
## Analytics
|
|
|
|
`AnalyticsTracker.log(_:properties:)` wraps Aptabase. Tracks: create, edit, update, delete, tab changes, navigation. In DEBUG builds, prints to NSLog.
|
|
|
|
## Component Library
|
|
|
|
`ComponentLibraryViewModel` fetches from a **PocketBase** backend with paginated API calls. Items have multi-language translations and locale-aware affiliate links (exact region match → language-only fallback → global fallback). Use `ComponentLibraryViewModel(previewItems:)` to bypass network in previews.
|
|
|
|
## Code Style
|
|
|
|
- 4-space indentation, trailing commas on multiline collections, 120-char soft line limit
|
|
- `UpperCamelCase` for types, `lowerCamelCase` for methods/properties
|
|
- Test naming: `testScenario_expectedResult`
|
|
- Commit messages: short imperative subjects under 50 characters
|
|
|
|
## Localization
|
|
|
|
5 languages: English (base), German, Spanish, French, Dutch. Translation files are in `*.lproj/Localizable.strings` and `Localizable.stringsdict`.
|
|
|
|
- Use `String(localized:defaultValue:)` — **not** `NSLocalizedString`. The `defaultValue` serves as English fallback and avoids showing raw keys when a Localizable.strings entry is missing.
|
|
- When adding new user-facing strings, add translations to **all 5** Localizable.strings files immediately.
|
|
|
|
## Export Options
|
|
|
|
Three export options, available from the Overview tab's share menu plus the BOM sheet. **Keep iOS and Android (`android/.../pdf/`) in sync — they must offer the same exports.**
|
|
|
|
1. **System Overview (PDF)** — summary + a full-page wiring diagram + per-entity tables.
|
|
2. **Bill of Materials (PDF)** — categorized component list.
|
|
3. **Wiring Diagram (PNG)** — standalone diagram image.
|
|
|
|
The wiring diagram (used both as the standalone PNG and the Overview PDF's diagram page) is fetched from the shared **VoltPlan diagram API** (`POST https://voltplan.app/api/diagram/generate`, JSON payload of system/loads/batteries/chargers, returns PNG). Both platforms send the identical payload shape; falls back gracefully when the API is unreachable (iOS draws a Core Graphics diagram; Android omits the PDF page / shows an error toast for the standalone export).
|
|
|
|
### PDF Export Pattern (iOS)
|
|
|
|
PDF exports use `UIGraphicsPDFRenderer` with A4 portrait format. The pattern is:
|
|
- **Exporter struct** (e.g. `SystemOverviewPDFExporter`, `SystemBillOfMaterialsPDFExporter`) with snapshot data types — keeps Core Graphics rendering isolated from SwiftUI/SwiftData.
|
|
- **ShareSheet** triggered via `@State` item binding in the parent view.
|
|
- **Toolbar button** (not inline content) for the export action.
|
|
|
|
On Android the equivalents live in `android/app/src/main/java/app/voltplan/cable/pdf/`: `SystemOverviewPdf`, `SystemBomPdf`, `SystemDiagram` (diagram fetch + PNG export), and `PdfShare` (`PdfDocument` + `FileProvider`/`ACTION_SEND`).
|
|
|
|
## Screenshots
|
|
|
|
App Store screenshots are generated via XCUITests running on simulators, automated by `shooter.sh`.
|
|
|
|
### Running screenshots
|
|
|
|
```bash
|
|
./shooter.sh # reads ./screenshot.config
|
|
./shooter.sh other.config # explicit config file
|
|
VERBOSE=1 ./shooter.sh # full logs on failure
|
|
PARALLEL=0 ./shooter.sh # sequential mode
|
|
```
|
|
|
|
Requires `xcparse`: `brew install chargepoint/xcparse/xcparse`.
|
|
|
|
### How it works
|
|
|
|
1. `shooter.sh` reads `screenshot.config` for scheme, bundle ID, devices, and languages.
|
|
2. Builds once with `build-for-testing`, then runs `test-without-building` per language.
|
|
3. Per language run: erases simulator, sets locale via `simctl spawn ... defaults write`, suppresses system notifications (DND, Apple Intelligence), overrides status bar (9:41, full battery).
|
|
4. `xcparse` extracts screenshot attachments from `.xcresult` bundles into `Shots/Screenshots/{device-slug}/{lang}/`.
|
|
5. Devices run in parallel (languages sequential per device — same simulator).
|
|
|
|
### Test structure
|
|
|
|
- **Test target**: `CableUITestsScreenshot` (scheme: `CableScreenshots`, test plan: `CableScreenshots.xctestplan`)
|
|
- **Test file**: `CableUITestsScreenshot/CableUITestsScreenshot.swift`
|
|
- **Sample data**: `UITestSampleData.swift` — seeded via `--uitest-sample-data` launch argument, cleared via `--uitest-reset-data`
|
|
|
|
### Screenshot inventory
|
|
|
|
| # | Name | Source |
|
|
|---|------|--------|
|
|
| 01 | OnboardingSystemsView | Empty state after reset |
|
|
| 02 | OnboardingSystemView | New system overview |
|
|
| 03 | LoadEditorView | CalculatorView for new load |
|
|
| 04 | ComponentSelectorView | Component library (network) |
|
|
| 05 | SystemsWithSampleData | Systems list with sample data |
|
|
| 06 | AdventureVanOverview | Overview tab |
|
|
| 07 | AdventureVanLoads | Components tab |
|
|
| 08 | BillOfMaterials | System BOM sheet |
|
|
| 09 | AdventureVanCalculator | Load calculator |
|
|
| 10 | AdventureVanBatteries | Batteries tab |
|
|
| 11 | BatteryEditor | Battery editor |
|
|
| 12 | AdventureVanChargers | Chargers tab |
|
|
| 13 | ChargerEditor | Charger editor |
|
|
|
|
### Accessibility identifiers for UI tests
|
|
|
|
Key identifiers used by the screenshot tests: `create-system-button`, `systems-list`, `system-overview`, `overview-tab`, `components-tab`, `batteries-tab`, `chargers-tab`, `loads-list`, `batteries-list`, `chargers-list`, `system-bom-button`, `system-bom-close-button`, `library-view-close-button`, `create-component-button`, `select-component-button`.
|
|
|
|
### Preview-friendly view inits
|
|
|
|
- **LoadsView** accepts `initialTab` (`LoadsView.ComponentTab`) to control which tab is shown.
|
|
- **ComponentLibraryView** accepts an optional `viewModel` via `init(viewModel:onSelect:)`. Use `ComponentLibraryViewModel(previewItems:)` to bypass network.
|
|
|
|
## Model Definitions
|
|
|
|
SwiftData `@Model` classes live in their feature directories: `ElectricalSystem` and `SavedLoad` in `Loads/CableCalculator.swift`, `SavedBattery` in `Batteries/SavedBattery.swift`, `SavedCharger` in `Chargers/SavedCharger.swift`. `Item.swift` is a legacy model.
|
|
|
|
## Xcode Project Structure
|
|
|
|
The project uses **automatic file inclusion** — no explicit file references in `project.pbxproj` for Swift sources. New `.swift` files in the `Cable/` directory are picked up automatically.
|
|
|
|
## Dependencies
|
|
|
|
- **SPM**: `aptabase-swift` (analytics) — referenced in `project.pbxproj`, resolved automatically by Xcode.
|
|
- **CocoaPods**: `Podfile` exists but has no pods listed — effectively unused.
|
|
- `Package.resolved` is in `xcshareddata` which is gitignored — it will be regenerated on a fresh checkout.
|
|
|
|
## SourceKit Diagnostics
|
|
|
|
SourceKit may show false-positive errors (e.g. "Cannot find type in scope") while the project builds successfully. These are indexing artifacts — always verify with an actual build via `mcp__xcode__BuildProject`.
|