Fix AWG notation, add alternator type, migrate to String(localized:)
- Fix AWG 0/00/000/0000 bug (all resolved to 0 in Swift) using negative int convention (-1 through -4) with formatAWG() for 1/0–4/0 display - Add 7.5A fuse size and change fuse type from Int to Double - Add alternator power source type with distinct bolt.car.fill icon - Migrate all NSLocalizedString calls to String(localized:defaultValue:) - Update translations for runtime subtitle (ES/FR/NL: current→maximum), usable capacity footer text, and NL override wording - Store length always in meters, convert at display time in CalculatorView - Add preview-friendly inits for ComponentLibraryView and LoadsView - Expand test coverage for calculations, fuses, AWG, and edge cases Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
110
CLAUDE.md
110
CLAUDE.md
@@ -19,7 +19,7 @@ No external dependencies beyond the Xcode toolchain.
|
||||
|
||||
### Data Model Hierarchy
|
||||
|
||||
`ElectricalSystem` (top-level container) owns collections of:
|
||||
`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
|
||||
@@ -29,8 +29,8 @@ All are `@Model` classes persisted via SwiftData. The container is configured in
|
||||
### 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.
|
||||
- **App-wide state**: `UnitSystemSettings` (metric/imperial, persisted to UserDefaults) and `StoreKitManager` (subscription status) are injected as `@EnvironmentObject`.
|
||||
- **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
|
||||
|
||||
@@ -38,7 +38,59 @@ All are `@Model` classes persisted via SwiftData. The container is configured in
|
||||
|
||||
### Feature Organization
|
||||
|
||||
Each feature has its own directory under `Cable/`: `Loads/`, `Systems/`, `Batteries/`, `Chargers/`, `Overview/`, `Paywall/`. Models and views for each feature live together.
|
||||
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
|
||||
|
||||
@@ -61,6 +113,52 @@ PDF exports use `UIGraphicsPDFRenderer` with A4 portrait format. The pattern is:
|
||||
- **ShareSheet** triggered via `@State` item binding in the parent view.
|
||||
- **Toolbar button** (not inline content) for the export action.
|
||||
|
||||
## StoreKit
|
||||
## Screenshots & Previews
|
||||
|
||||
Subscription product IDs: `app.voltplan.cable.weekly`, `app.voltplan.cable.yearly`. Pro features are gated via `StoreKitManager.isPro`.
|
||||
Screenshot previews for all major views live in `Cable/ScreenshotPreviews.swift`. This file contains wrapper views and realistic sample data so screenshots include full app chrome (NavigationBar, TabBar).
|
||||
|
||||
### How to render screenshots
|
||||
|
||||
1. Use `mcp__xcode__RenderPreview` with `sourceFilePath: "Cable/ScreenshotPreviews.swift"` and `previewDefinitionIndexInFile` (0–44, grouped by view × 5 languages).
|
||||
2. Output goes to `Shots/Screenshots/`.
|
||||
|
||||
### Preview index mapping
|
||||
|
||||
Previews are grouped in blocks of 5 (EN, DE, ES, FR, NL):
|
||||
- 0–4: Overview tab
|
||||
- 5–9: Components tab
|
||||
- 10–14: Batteries tab
|
||||
- 15–19: Chargers tab
|
||||
- 20–24: Systems list
|
||||
- 25–29: Parts Library
|
||||
- 30–34: Load editor (CalculatorView)
|
||||
- 35–39: Battery editor
|
||||
- 40–44: Charger editor
|
||||
|
||||
### Key patterns for preview-friendly views
|
||||
|
||||
- **LoadsView** accepts an `initialTab` parameter (`LoadsView.ComponentTab`) to control which tab is shown. Wrap in `NavigationStack` to get the navigation bar.
|
||||
- **ComponentLibraryView** accepts an optional `viewModel` parameter via `init(viewModel:onSelect:)`. Use `ComponentLibraryViewModel(previewItems:)` to inject mock data and avoid network dependency.
|
||||
- All preview wrappers inject `.modelContainer(container)` (in-memory) and `.environmentObject(UnitSystemSettings())`.
|
||||
|
||||
### Localization limitation
|
||||
|
||||
`.environment(\.locale, Locale(identifier: "xx"))` does **not** affect `String(localized:defaultValue:)` — those resolve from the app bundle, not the SwiftUI environment. All preview screenshots render in the system language. For multi-language screenshots, use UI tests with `-AppleLanguages` launch arguments or change the Xcode scheme language.
|
||||
|
||||
## 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`.
|
||||
|
||||
Reference in New Issue
Block a user