Files
Cable/CLAUDE.md
Stefan Lange-Hegermann ea3b60d75c 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>
2026-03-27 10:37:53 +01:00

9.7 KiB
Raw Blame History

CLAUDE.md

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

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:

@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.

PDF Export Pattern

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.

Screenshots & Previews

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 (044, 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):

  • 04: Overview tab
  • 59: Components tab
  • 1014: Batteries tab
  • 1519: Chargers tab
  • 2024: Systems list
  • 2529: Parts Library
  • 3034: Load editor (CalculatorView)
  • 3539: Battery editor
  • 4044: 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.