diff --git a/Cable/Base.lproj/Localizable.strings b/Cable/Base.lproj/Localizable.strings index 595498b..8d7501a 100644 --- a/Cable/Base.lproj/Localizable.strings +++ b/Cable/Base.lproj/Localizable.strings @@ -80,6 +80,9 @@ "loads.overview.empty.message" = "Start by adding a load to see system insights."; "loads.overview.empty.create" = "Add Load"; "loads.overview.empty.library" = "Browse Library"; +"loads.library.button" = "Library"; +"loads.onboarding.title" = "Add your first component"; +"loads.onboarding.subtitle" = "Bring your system to life with components and let **Cable by VoltPlan** handle cable and fuse recommendations."; "loads.overview.status.missing_details.title" = "Missing load details"; "loads.overview.status.missing_details.message" = "Enter cable length and wire size for %d %@ to see accurate recommendations."; "loads.overview.status.missing_details.singular" = "load"; @@ -102,6 +105,8 @@ "battery.bank.metric.capacity" = "Capacity"; "battery.bank.metric.energy" = "Energy"; "battery.overview.empty.create" = "Add Battery"; +"battery.onboarding.title" = "Add your first battery"; +"battery.onboarding.subtitle" = "Track your bank's capacity and chemistry to keep runtime expectations in check."; "battery.bank.badge.voltage" = "Voltage"; "battery.bank.badge.capacity" = "Capacity"; "battery.bank.badge.energy" = "Energy"; diff --git a/Cable/BatteriesView.swift b/Cable/Batteries/BatteriesView.swift similarity index 100% rename from Cable/BatteriesView.swift rename to Cable/Batteries/BatteriesView.swift diff --git a/Cable/BatteryConfiguration.swift b/Cable/Batteries/BatteryConfiguration.swift similarity index 100% rename from Cable/BatteryConfiguration.swift rename to Cable/Batteries/BatteryConfiguration.swift diff --git a/Cable/BatteryEditorView.swift b/Cable/Batteries/BatteryEditorView.swift similarity index 99% rename from Cable/BatteryEditorView.swift rename to Cable/Batteries/BatteryEditorView.swift index 4ae7ec1..7ff8296 100644 --- a/Cable/BatteryEditorView.swift +++ b/Cable/Batteries/BatteryEditorView.swift @@ -232,7 +232,7 @@ struct BatteryEditorView: View { voltageInput = formattedEditValue(configuration.nominalVoltage) } } - .onChange(of: voltageInput) { newValue in + .onChange(of: voltageInput) { _, newValue in guard editingField == .voltage, let parsed = parseInput(newValue) else { return } configuration.nominalVoltage = roundToTenth(parsed) } @@ -306,7 +306,7 @@ struct BatteryEditorView: View { capacityInput = formattedEditValue(configuration.capacityAmpHours) } } - .onChange(of: capacityInput) { newValue in + .onChange(of: capacityInput) { _, newValue in guard editingField == .capacity, let parsed = parseInput(newValue) else { return } configuration.capacityAmpHours = roundToTenth(parsed) } diff --git a/Cable/ChargersView.swift b/Cable/Chargers/ChargersView.swift similarity index 100% rename from Cable/ChargersView.swift rename to Cable/Chargers/ChargersView.swift diff --git a/Cable/ComponentsOnboardingView.swift b/Cable/ComponentsOnboardingView.swift deleted file mode 100644 index 46aecf1..0000000 --- a/Cable/ComponentsOnboardingView.swift +++ /dev/null @@ -1,122 +0,0 @@ -import SwiftUI - -struct ComponentsOnboardingView: View { - @State private var carouselStep = 0 - let onCreate: () -> Void - let onBrowse: () -> Void - - private let imageNames = [ - "coffee-onboarding", - "router-onboarding", - "charger-onboarding" - ] - - private let timer = Timer.publish(every: 8, on: .main, in: .common).autoconnect() - private let animationDuration = 0.8 - - private var loopingImages: [String] { - guard let first = imageNames.first else { return [] } - return imageNames + [first] - } - - var body: some View { - VStack { - Spacer(minLength: 32) - - OnboardingCarouselView(images: loopingImages, step: carouselStep) - .frame(minHeight: 80, maxHeight: 240) - .padding(.horizontal, 0) - - VStack(spacing: 12) { - Text("Add your first component") - .font(.title2.weight(.semibold)) - .multilineTextAlignment(.center) - - Text("Bring your system to life with components and let **Cable by VoltPlan** handle cable and fuse recommendations.") - .font(.body) - .foregroundStyle(Color.secondary) - .multilineTextAlignment(.center) - .frame(minHeight: 72) - .padding(.horizontal, 12) - } - .padding(.horizontal, 24) - - Spacer() - - VStack(spacing: 12) { - Button(action: createComponent) { - HStack(spacing: 8) { - Image(systemName: "plus.circle.fill") - .font(.system(size: 16)) - Text("Create Component") - .font(.headline.weight(.semibold)) - } - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 48) - .background(Color.blue) - .cornerRadius(12) - } - .accessibilityIdentifier("create-component-button") - .buttonStyle(.plain) - - Button(action: onBrowse) { - HStack(spacing: 8) { - Image(systemName: "books.vertical") - .font(.system(size: 16)) - Text("Browse Library") - .font(.headline.weight(.semibold)) - } - .foregroundColor(.blue) - .frame(maxWidth: .infinity) - .frame(height: 48) - .background( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(Color.blue.opacity(0.12)) - ) - .overlay( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .stroke(Color.blue.opacity(0.24), lineWidth: 1) - ) - } - .accessibilityIdentifier("select-component-button") - .buttonStyle(.plain) - } - .padding(.horizontal, 24) - } - .padding(.bottom, 32) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(.systemGroupedBackground)) - .onAppear(perform: resetState) - .onReceive(timer) { _ in advanceCarousel() } - } - - private func resetState() { - carouselStep = 0 - } - - private func createComponent() { - onCreate() - } - - private func advanceCarousel() { - guard imageNames.count > 1 else { return } - let next = carouselStep + 1 - - withAnimation(.easeInOut(duration: animationDuration)) { - carouselStep = next - } - - if next == imageNames.count { - DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) { - withAnimation(.none) { - carouselStep = 0 - } - } - } - } -} - -#Preview { - ComponentsOnboardingView(onCreate: {}, onBrowse: {}) -} diff --git a/Cable/CableCalculator.swift b/Cable/Loads/CableCalculator.swift similarity index 100% rename from Cable/CableCalculator.swift rename to Cable/Loads/CableCalculator.swift diff --git a/Cable/CalculatorView.swift b/Cable/Loads/CalculatorView.swift similarity index 96% rename from Cable/CalculatorView.swift rename to Cable/Loads/CalculatorView.swift index 1633aa2..f4ab0ec 100644 --- a/Cable/CalculatorView.swift +++ b/Cable/Loads/CalculatorView.swift @@ -151,10 +151,10 @@ struct CalculatorView: View { lengthInput = formattedValue(calculator.length) } } - .onChange(of: lengthInput) { newValue in - guard editingValue == .length, let parsed = parseInput(newValue) else { return } - calculator.length = roundToTenth(parsed) - } + .onChange(of: lengthInput) { _, newValue in + guard editingValue == .length, let parsed = parseInput(newValue) else { return } + calculator.length = roundToTenth(parsed) + } Button("Cancel", role: .cancel) { editingValue = nil lengthInput = "" @@ -187,10 +187,10 @@ struct CalculatorView: View { voltageInput = formattedValue(calculator.voltage) } } - .onChange(of: voltageInput) { newValue in - guard editingValue == .voltage, let parsed = parseInput(newValue) else { return } - calculator.voltage = roundToTenth(parsed) - } + .onChange(of: voltageInput) { _, newValue in + guard editingValue == .voltage, let parsed = parseInput(newValue) else { return } + calculator.voltage = roundToTenth(parsed) + } Button("Cancel", role: .cancel) { editingValue = nil voltageInput = "" @@ -228,10 +228,10 @@ struct CalculatorView: View { currentInput = formattedValue(calculator.current) } } - .onChange(of: currentInput) { newValue in - guard editingValue == .current, let parsed = parseInput(newValue) else { return } - calculator.current = roundToTenth(parsed) - } + .onChange(of: currentInput) { _, newValue in + guard editingValue == .current, let parsed = parseInput(newValue) else { return } + calculator.current = roundToTenth(parsed) + } Button("Cancel", role: .cancel) { editingValue = nil currentInput = "" @@ -265,10 +265,10 @@ struct CalculatorView: View { powerInput = formattedValue(calculator.power) } } - .onChange(of: powerInput) { newValue in - guard editingValue == .power, let parsed = parseInput(newValue) else { return } - calculator.power = roundToNearestFive(parsed) - } + .onChange(of: powerInput) { _, newValue in + guard editingValue == .power, let parsed = parseInput(newValue) else { return } + calculator.power = roundToNearestFive(parsed) + } Button("Cancel", role: .cancel) { editingValue = nil powerInput = "" @@ -824,7 +824,7 @@ struct CalculatorView: View { unit: "V", tapAction: beginVoltageEditing, snapValues: editingValue == .voltage ? nil : voltageSnapValues) - .onChange(of: calculator.voltage) { + .onChange(of: calculator.voltage) { _, _ in if isWattMode { calculator.updateFromPower() } else { @@ -859,7 +859,7 @@ struct CalculatorView: View { }, tapAction: beginPowerEditing, snapValues: editingValue == .power ? nil : powerSnapValues) - .onChange(of: calculator.power) { + .onChange(of: calculator.power) { _, _ in calculator.updateFromPower() calculator.objectWillChange.send() autoUpdateSavedLoad() @@ -886,7 +886,7 @@ struct CalculatorView: View { }, tapAction: beginCurrentEditing, snapValues: editingValue == .current ? nil : currentSnapValues) - .onChange(of: calculator.current) { + .onChange(of: calculator.current) { _, _ in calculator.updateFromCurrent() calculator.objectWillChange.send() autoUpdateSavedLoad() @@ -914,10 +914,10 @@ struct CalculatorView: View { unit: unitSettings.unitSystem.lengthUnit, tapAction: beginLengthEditing, snapValues: editingValue == .length ? nil : lengthSnapValues) - .onChange(of: calculator.length) { - calculator.objectWillChange.send() - autoUpdateSavedLoad() - } + .onChange(of: calculator.length) { _, _ in + calculator.objectWillChange.send() + autoUpdateSavedLoad() + } } private func normalizedVoltage(for value: Double) -> Double { @@ -1239,17 +1239,21 @@ struct SliderSection: View { .foregroundColor(.secondary) Slider(value: $value, in: range) - .onChange(of: value) { + .onChange(of: value) { _, newValue in // Always round to 1 decimal place first - value = round(value * 10) / 10 - + var adjusted = (newValue * 10).rounded() / 10 + if let snapValues = snapValues { // Find the closest snap value - let closest = snapValues.min { abs($0 - value) < abs($1 - value) } - if let closest = closest, abs(closest - value) < 0.5 { - value = closest + if let closest = snapValues.min(by: { abs($0 - adjusted) < abs($1 - adjusted) }), + abs(closest - adjusted) < 0.5 { + adjusted = closest } } + + if abs(adjusted - newValue) > 0.000001 { + value = adjusted + } } Text(String(format: "%.0f%@", range.upperBound, unit)) diff --git a/Cable/ComponentLibraryView.swift b/Cable/Loads/ComponentLibraryView.swift similarity index 98% rename from Cable/ComponentLibraryView.swift rename to Cable/Loads/ComponentLibraryView.swift index c5e25c6..390b85c 100644 --- a/Cable/ComponentLibraryView.swift +++ b/Cable/Loads/ComponentLibraryView.swift @@ -126,15 +126,6 @@ struct ComponentLibraryItem: Identifiable, Equatable { append(locale.identifier) - let components = Locale.components(fromIdentifier: locale.identifier) - - if let language = components[NSLocale.Key.languageCode.rawValue]?.lowercased() { - if let region = components[NSLocale.Key.countryCode.rawValue]?.uppercased() { - append("\(language)_\(region)") - } - append(language) - } - if let languageCode = locale.language.languageCode?.identifier.lowercased() { append(languageCode) } diff --git a/Cable/LoadConfigurationStatus.swift b/Cable/Loads/LoadConfigurationStatus.swift similarity index 100% rename from Cable/LoadConfigurationStatus.swift rename to Cable/Loads/LoadConfigurationStatus.swift diff --git a/Cable/LoadEditorView.swift b/Cable/Loads/LoadEditorView.swift similarity index 100% rename from Cable/LoadEditorView.swift rename to Cable/Loads/LoadEditorView.swift diff --git a/Cable/LoadIconView.swift b/Cable/Loads/LoadIconView.swift similarity index 100% rename from Cable/LoadIconView.swift rename to Cable/Loads/LoadIconView.swift diff --git a/Cable/LoadsView.swift b/Cable/Loads/LoadsView.swift similarity index 88% rename from Cable/LoadsView.swift rename to Cable/Loads/LoadsView.swift index 446629d..c005bfa 100644 --- a/Cable/LoadsView.swift +++ b/Cable/Loads/LoadsView.swift @@ -70,25 +70,35 @@ struct LoadsView: View { systemImage: "square.stack.3d.up" ) } - BatteriesView( - system: system, - batteries: savedBatteries, - editMode: $editMode, - onEdit: { editBattery($0) }, - onDelete: deleteBatteries + Group { + if savedBatteries.isEmpty { + OnboardingInfoView( + configuration: .battery(), + onPrimaryAction: { startBatteryConfiguration() } + ) + } else { + BatteriesView( + system: system, + batteries: savedBatteries, + editMode: $editMode, + onEdit: { editBattery($0) }, + onDelete: deleteBatteries + ) + .environment(\.editMode, $editMode) + } + } + .tag(ComponentTab.batteries) + .tabItem { + Label( + String( + localized: "tab.batteries", + bundle: .main, + comment: "Tab title for battery configurations" + ), + systemImage: "battery.100" ) - .environment(\.editMode, $editMode) - .tag(ComponentTab.batteries) - .tabItem { - Label( - String( - localized: "tab.batteries", - bundle: .main, - comment: "Tab title for battery configurations" - ), - systemImage: "battery.100" - ) - } + } + .environment(\.editMode, $editMode) ChargersView(system: system) .tag(ComponentTab.chargers) .tabItem { @@ -129,35 +139,27 @@ struct LoadsView: View { } ToolbarItem(placement: .navigationBarTrailing) { - HStack { - if selectedComponentTab == .components { - Button(action: { - showingComponentLibrary = true - }) { - Image(systemName: "books.vertical") + let showPrimary = selectedComponentTab != .overview + let showEditLoads = selectedComponentTab == .components && !savedLoads.isEmpty + let showEditBatteries = selectedComponentTab == .batteries && !savedBatteries.isEmpty + + if showPrimary || showEditLoads || showEditBatteries { + HStack { + if showPrimary { + Button(action: { + handlePrimaryAction() + }) { + Image(systemName: "plus") + } + .disabled(selectedComponentTab == .chargers) } - .accessibilityIdentifier("component-library-button") - } - if !savedLoads.isEmpty && (selectedComponentTab == .components || selectedComponentTab == .overview) { - Button(action: { - showingSystemBOM = true - }) { - Image(systemName: "list.bullet.rectangle") + if showEditLoads { + EditButton() + .disabled(savedLoads.isEmpty) + } else if showEditBatteries { + EditButton() + .disabled(savedBatteries.isEmpty) } - .accessibilityIdentifier("system-bom-button") - } - Button(action: { - handlePrimaryAction() - }) { - Image(systemName: "plus") - } - .disabled(selectedComponentTab == .chargers) - if selectedComponentTab == .components { - EditButton() - .disabled(savedLoads.isEmpty) - } else if selectedComponentTab == .batteries { - EditButton() - .disabled(savedBatteries.isEmpty) } } } @@ -267,6 +269,29 @@ struct LoadsView: View { Text(loadsSummaryTitle) .font(.headline.weight(.semibold)) Spacer() + Button { + showingComponentLibrary = true + } label: { + HStack(spacing: 6) { + Image(systemName: "books.vertical") + Text( + String( + localized: "loads.library.button", + bundle: .main, + comment: "Button title to open component library" + ) + ) + } + .font(.footnote.weight(.semibold)) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background( + Capsule(style: .continuous) + .fill(Color(.tertiarySystemFill)) + ) + } + .buttonStyle(.plain) + .foregroundStyle(Color.accentColor) } ViewThatFits(in: .horizontal) { @@ -333,19 +358,16 @@ struct LoadsView: View { private var componentsTab: some View { VStack(spacing: 0) { - summarySection - if savedLoads.isEmpty { - ScrollView { - ComponentsOnboardingView( - onCreate: { createNewLoad() }, - onBrowse: { showingComponentLibrary = true } - ) - .padding(.horizontal, 16) - .padding(.top, 32) - .padding(.bottom, 24) - } + OnboardingInfoView( + configuration: .loads(), + onPrimaryAction: { createNewLoad() }, + onSecondaryAction: { showingComponentLibrary = true } + ) + .padding(.horizontal, 0) } else { + summarySection + List { ForEach(savedLoads) { load in Button { diff --git a/Cable/Loads/OnboardingInfoView.swift b/Cable/Loads/OnboardingInfoView.swift new file mode 100644 index 0000000..7033305 --- /dev/null +++ b/Cable/Loads/OnboardingInfoView.swift @@ -0,0 +1,149 @@ +import SwiftUI + +struct OnboardingInfoView: View { + struct Configuration { + let title: LocalizedStringKey + let subtitle: LocalizedStringKey + let primaryActionTitle: LocalizedStringKey + let primaryActionIcon: String + let secondaryActionTitle: LocalizedStringKey? + let secondaryActionIcon: String? + let imageNames: [String] + } + + @State private var carouselStep = 0 + private let configuration: Configuration + private let onPrimaryAction: () -> Void + private let onSecondaryAction: () -> Void + + private let timer = Timer.publish(every: 8, on: .main, in: .common).autoconnect() + private let animationDuration = 0.8 + + private var loopingImages: [String] { + guard let first = configuration.imageNames.first else { return [] } + return configuration.imageNames + [first] + } + + init(configuration: Configuration, onPrimaryAction: @escaping () -> Void, onSecondaryAction: @escaping () -> Void = {}) { + self.configuration = configuration + self.onPrimaryAction = onPrimaryAction + self.onSecondaryAction = onSecondaryAction + } + + var body: some View { + VStack(spacing: 24) { + Spacer(minLength: 32) + + if !loopingImages.isEmpty { + OnboardingCarouselView(images: loopingImages, step: carouselStep) + .frame(minHeight: 80, maxHeight: 220) + .padding(.horizontal, 0) + } + + VStack(spacing: 12) { + Text(configuration.title) + .font(.title2.weight(.semibold)) + .multilineTextAlignment(.center) + + Text(configuration.subtitle) + .font(.body) + .foregroundStyle(Color.secondary) + .multilineTextAlignment(.center) + .frame(minHeight: 72) + .padding(.horizontal, 12) + } + .padding(.horizontal, 24) + + Spacer() + + VStack(spacing: 12) { + Button(action: onPrimaryAction) { + Label(configuration.primaryActionTitle, systemImage: configuration.primaryActionIcon) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + + if let secondaryTitle = configuration.secondaryActionTitle, + let secondaryIcon = configuration.secondaryActionIcon { + Button(action: onSecondaryAction) { + Label(secondaryTitle, systemImage: secondaryIcon) + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .tint(.accentColor) + .controlSize(.large) + } + } + .padding(.horizontal, 24) + } + .padding(.vertical, 24) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(.systemGroupedBackground)) + .onAppear(perform: resetState) + .onReceive(timer) { _ in advanceCarousel() } + } + + private func resetState() { + carouselStep = 0 + } + + private func advanceCarousel() { + guard configuration.imageNames.count > 1 else { return } + let next = carouselStep + 1 + + withAnimation(.easeInOut(duration: animationDuration)) { + carouselStep = next + } + + if next == configuration.imageNames.count { + DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) { + withAnimation(.none) { + carouselStep = 0 + } + } + } + } +} + +#Preview { + OnboardingInfoView( + configuration: .loads(), + onPrimaryAction: {}, + onSecondaryAction: {} + ) +} + +extension OnboardingInfoView.Configuration { + static func loads() -> Self { + Self( + title: LocalizedStringKey("loads.onboarding.title"), + subtitle: LocalizedStringKey("loads.onboarding.subtitle"), + primaryActionTitle: LocalizedStringKey("loads.overview.empty.create"), + primaryActionIcon: "plus", + secondaryActionTitle: LocalizedStringKey("loads.overview.empty.library"), + secondaryActionIcon: "books.vertical", + imageNames: [ + "coffee-onboarding", + "router-onboarding", + "charger-onboarding" + ] + ) + } + + static func battery() -> Self { + Self( + title: LocalizedStringKey("battery.onboarding.title"), + subtitle: LocalizedStringKey("battery.onboarding.subtitle"), + primaryActionTitle: LocalizedStringKey("battery.overview.empty.create"), + primaryActionIcon: "plus", + secondaryActionTitle: nil, + secondaryActionIcon: nil, + imageNames: [ + "charger-onboarding", + "router-onboarding", + "coffee-onboarding" + ] + ) + } +} diff --git a/Cable/SystemOverviewView.swift b/Cable/Overview/SystemOverviewView.swift similarity index 93% rename from Cable/SystemOverviewView.swift rename to Cable/Overview/SystemOverviewView.swift index c64aee2..8d3a04a 100644 --- a/Cable/SystemOverviewView.swift +++ b/Cable/Overview/SystemOverviewView.swift @@ -1,6 +1,7 @@ import SwiftUI struct SystemOverviewView: View { + @Environment(\.dismiss) private var dismiss @State private var activeStatus: LoadConfigurationStatus? @State private var suppressLoadNavigation = false let system: ElectricalSystem @@ -771,3 +772,85 @@ struct SystemOverviewView: View { } } } + +#Preview("SystemOverview – Populated") { + let system = ElectricalSystem( + name: "12V DC System", + location: "Engine Room", + iconName: "bolt.circle.fill", + colorName: "blue" + ) + + let loads: [SavedLoad] = [ + SavedLoad( + name: "Navigation Lights", + voltage: 12.8, + current: 2.4, + power: 28.8, + length: 5.0, + crossSection: 2.5 + ), + SavedLoad( + name: "Bilge Pump", + voltage: 12.8, + current: 8.0, + power: 96.0, + length: 3.0, + crossSection: 4.0 + ), + SavedLoad( + name: "Chartplotter", + voltage: 12.8, + current: 1.5, + power: 18.0, + length: 2.0, + crossSection: 1.5 + ) + ] + + let batteries: [SavedBattery] = [ + SavedBattery( + name: "House AGM", + nominalVoltage: 12.0, + capacityAmpHours: 100.0 + ), + SavedBattery( + name: "Starter AGM", + nominalVoltage: 12.0, + capacityAmpHours: 100.0 + ) + ] + + SystemOverviewView( + system: system, + loads: loads, + batteries: batteries, + onSelectLoads: {}, + onSelectBatteries: {}, + onCreateLoad: {}, + onBrowseLibrary: {}, + onCreateBattery: {} + ) + .padding() +} + +#Preview("SystemOverview – Empty States") { + let system = ElectricalSystem( + name: "24V DC System", + location: "Main Panel", + iconName: "bolt.circle.fill", + colorName: "green" + ) + + return SystemOverviewView( + system: system, + loads: [], + batteries: [], + onSelectLoads: {}, + onSelectBatteries: {}, + onCreateLoad: {}, + onBrowseLibrary: {}, + onCreateBattery: {} + ) + .padding() +} diff --git a/Cable/SystemView.swift b/Cable/SystemView.swift deleted file mode 100644 index ae225ca..0000000 --- a/Cable/SystemView.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// SystemView.swift -// Cable -// -// Created by Stefan Lange-Hegermann on 09.10.25. -// - - -import SwiftUI -import SwiftData - -struct SystemView: View { - var body: some View { - NavigationStack { - VStack(spacing: 24) { - Spacer() - - VStack(spacing: 16) { - Image(systemName: "square.grid.3x2") - .font(.system(size: 48)) - .foregroundColor(.secondary) - - Text("System View") - .font(.title2) - .fontWeight(.semibold) - - Text("Coming soon - manage your electrical systems and panels here.") - .font(.body) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal, 48) - } - - Spacer() - Spacer() - } - .navigationTitle("System") - } - } -} \ No newline at end of file diff --git a/Cable/SystemBillOfMaterialsView.swift b/Cable/Systems/SystemBillOfMaterialsView.swift similarity index 100% rename from Cable/SystemBillOfMaterialsView.swift rename to Cable/Systems/SystemBillOfMaterialsView.swift diff --git a/Cable/SystemComponentsPersistence.swift b/Cable/Systems/SystemComponentsPersistence.swift similarity index 100% rename from Cable/SystemComponentsPersistence.swift rename to Cable/Systems/SystemComponentsPersistence.swift diff --git a/Cable/SystemEditorView.swift b/Cable/Systems/SystemEditorView.swift similarity index 100% rename from Cable/SystemEditorView.swift rename to Cable/Systems/SystemEditorView.swift diff --git a/Cable/SystemsOnboardingView.swift b/Cable/Systems/SystemsOnboardingView.swift similarity index 100% rename from Cable/SystemsOnboardingView.swift rename to Cable/Systems/SystemsOnboardingView.swift diff --git a/Cable/SystemsView.swift b/Cable/Systems/SystemsView.swift similarity index 87% rename from Cable/SystemsView.swift rename to Cable/Systems/SystemsView.swift index ad52e94..1dcc728 100644 --- a/Cable/SystemsView.swift +++ b/Cable/Systems/SystemsView.swift @@ -414,3 +414,79 @@ struct SystemsView: View { } } } + +#Preview("Sample Systems") { + // An in-memory SwiftData container for previews so we don't persist anything + let configuration = ModelConfiguration(isStoredInMemoryOnly: true) + let container = try! ModelContainer(for: ElectricalSystem.self, SavedLoad.self, configurations: configuration) + + // Seed sample data only once per preview session + if (try? ModelContext(container).fetch(FetchDescriptor()))?.isEmpty ?? true { + let context = ModelContext(container) + + // Sample systems + let system1 = ElectricalSystem(name: "Camper Van", location: "Road Trip", iconName: "bus", colorName: "teal") + let system2 = ElectricalSystem(name: "Sailboat Aurora", location: "Marina 7", iconName: "sailboat", colorName: "blue") + + context.insert(system1) + context.insert(system2) + + // Sample loads for system 1 + let load1 = SavedLoad( + name: "LED Cabin Light", + voltage: 12, + current: 0.5, + power: 6, + length: 5, + crossSection: 1.5, + iconName: "lightbulb", + colorName: "yellow", + isWattMode: false, + system: system1, + remoteIconURLString: nil, + affiliateURLString: nil, + affiliateCountryCode: nil + ) + + let load2 = SavedLoad( + name: "Water Pump", + voltage: 12, + current: 5, + power: 60, + length: 3, + crossSection: 2.5, + iconName: "drop", + colorName: "blue", + isWattMode: false, + system: system1, + remoteIconURLString: nil, + affiliateURLString: nil, + affiliateCountryCode: nil + ) + + // Sample loads for system 2 + let load3 = SavedLoad( + name: "Navigation Lights", + voltage: 12, + current: 1.2, + power: 14.4, + length: 8, + crossSection: 1.5, + iconName: "lightbulb", + colorName: "green", + isWattMode: false, + system: system2, + remoteIconURLString: nil, + affiliateURLString: nil, + affiliateCountryCode: nil + ) + + context.insert(load1) + context.insert(load2) + context.insert(load3) + } + + return SystemsView() + .modelContainer(container) + .environmentObject(UnitSystemSettings()) +} diff --git a/Cable/de.lproj/Localizable.strings b/Cable/de.lproj/Localizable.strings index dd68338..1f435c6 100644 --- a/Cable/de.lproj/Localizable.strings +++ b/Cable/de.lproj/Localizable.strings @@ -141,13 +141,16 @@ "tab.batteries" = "Batterien"; "tab.chargers" = "Ladegeräte"; -"loads.overview.header.title" = "Verbraucherübersicht"; +"loads.overview.header.title" = "Verbraucher"; "loads.overview.metric.count" = "Verbraucher"; "loads.overview.metric.current" = "Strom"; "loads.overview.metric.power" = "Leistung"; "loads.overview.empty.message" = "Füge einen Verbraucher hinzu, um dein System einzurichten."; "loads.overview.empty.create" = "Verbraucher hinzufügen"; "loads.overview.empty.library" = "Bibliothek durchsuchen"; +"loads.library.button" = "Bibliothek"; +"loads.onboarding.title" = "Füge deinen ersten Verbraucher hinzu"; +"loads.onboarding.subtitle" = "Statte dein System mit Verbrauchern aus und lass **Cable by VoltPlan** die Kabel- und Sicherungsempfehlungen übernehmen."; "loads.overview.status.missing_details.title" = "Fehlende Verbraucherdetails"; "loads.overview.status.missing_details.message" = "Gib Kabellänge und Leitungsquerschnitt für %d %@ ein, um genaue Empfehlungen zu erhalten."; "loads.overview.status.missing_details.singular" = "Verbraucher"; @@ -165,11 +168,13 @@ "battery.bank.warning.voltage.short" = "Spannung"; "battery.bank.warning.capacity.short" = "Kapazität"; -"battery.bank.header.title" = "Batteriebank"; +"battery.bank.header.title" = "Batterien"; "battery.bank.metric.count" = "Batterien"; "battery.bank.metric.capacity" = "Kapazität"; "battery.bank.metric.energy" = "Energie"; "battery.overview.empty.create" = "Batterie hinzufügen"; +"battery.onboarding.title" = "Füge deine erste Batterie hinzu"; +"battery.onboarding.subtitle" = "Behalte Kapazität und Chemie deiner Batterien im Blick, um die Laufzeit im Griff zu behalten."; "battery.bank.badge.voltage" = "Spannung"; "battery.bank.badge.capacity" = "Kapazität"; "battery.bank.badge.energy" = "Energie"; diff --git a/Cable/es.lproj/Localizable.strings b/Cable/es.lproj/Localizable.strings index b30a8c3..8078285 100644 --- a/Cable/es.lproj/Localizable.strings +++ b/Cable/es.lproj/Localizable.strings @@ -147,6 +147,9 @@ "loads.overview.empty.message" = "Añade una carga para ver los detalles del sistema."; "loads.overview.empty.create" = "Añadir carga"; "loads.overview.empty.library" = "Explorar biblioteca"; +"loads.library.button" = "Biblioteca"; +"loads.onboarding.title" = "Añade tu primer consumidor"; +"loads.onboarding.subtitle" = "Completa tu sistema con consumidores y deja que **Cable by VoltPlan** calcule cables y fusibles por ti."; "loads.overview.status.missing_details.title" = "Faltan detalles de la carga"; "loads.overview.status.missing_details.message" = "Introduce la longitud del cable y el calibre del conductor para %d %@ para obtener recomendaciones precisas."; "loads.overview.status.missing_details.singular" = "carga"; @@ -169,6 +172,8 @@ "battery.bank.metric.capacity" = "Capacidad"; "battery.bank.metric.energy" = "Energía"; "battery.overview.empty.create" = "Añadir batería"; +"battery.onboarding.title" = "Añade tu primera batería"; +"battery.onboarding.subtitle" = "Controla la capacidad y la química del banco para mantener tus tiempos de autonomía bajo control."; "battery.bank.badge.voltage" = "Voltaje"; "battery.bank.badge.capacity" = "Capacidad"; "battery.bank.badge.energy" = "Energía"; diff --git a/Cable/fr.lproj/Localizable.strings b/Cable/fr.lproj/Localizable.strings index 4c0466a..2a8aba7 100644 --- a/Cable/fr.lproj/Localizable.strings +++ b/Cable/fr.lproj/Localizable.strings @@ -147,6 +147,9 @@ "loads.overview.empty.message" = "Ajoutez une charge pour voir les informations du système."; "loads.overview.empty.create" = "Ajouter une charge"; "loads.overview.empty.library" = "Parcourir la bibliothèque"; +"loads.library.button" = "Bibliothèque"; +"loads.onboarding.title" = "Ajoutez votre premier consommateur"; +"loads.onboarding.subtitle" = "Complétez votre système avec des équipements et laissez **Cable by VoltPlan** proposer les câbles et fusibles adaptés."; "loads.overview.status.missing_details.title" = "Détails de charge manquants"; "loads.overview.status.missing_details.message" = "Saisissez la longueur de câble et la section du conducteur pour %d %@ afin d'obtenir des recommandations précises."; "loads.overview.status.missing_details.singular" = "charge"; @@ -169,6 +172,8 @@ "battery.bank.metric.capacity" = "Capacité"; "battery.bank.metric.energy" = "Énergie"; "battery.overview.empty.create" = "Ajouter une batterie"; +"battery.onboarding.title" = "Ajoutez votre première batterie"; +"battery.onboarding.subtitle" = "Suivez la capacité et la chimie de votre banc pour mieux maîtriser l'autonomie."; "battery.bank.badge.voltage" = "Tension"; "battery.bank.badge.capacity" = "Capacité"; "battery.bank.badge.energy" = "Énergie"; diff --git a/Cable/nl.lproj/Localizable.strings b/Cable/nl.lproj/Localizable.strings index fbe290e..59a547e 100644 --- a/Cable/nl.lproj/Localizable.strings +++ b/Cable/nl.lproj/Localizable.strings @@ -147,6 +147,9 @@ "loads.overview.empty.message" = "Voeg een belasting toe om systeeminformatie te zien."; "loads.overview.empty.create" = "Belasting toevoegen"; "loads.overview.empty.library" = "Bibliotheek bekijken"; +"loads.library.button" = "Bibliotheek"; +"loads.onboarding.title" = "Voeg je eerste verbruiker toe"; +"loads.onboarding.subtitle" = "Bouw je systeem uit met verbruikers en laat **Cable by VoltPlan** de kabel- en zekeringadviezen verzorgen."; "loads.overview.status.missing_details.title" = "Ontbrekende lastdetails"; "loads.overview.status.missing_details.message" = "Voer kabellengte en kabeldoorsnede in voor %d %@ om nauwkeurige aanbevelingen te krijgen."; "loads.overview.status.missing_details.singular" = "last"; @@ -169,6 +172,8 @@ "battery.bank.metric.capacity" = "Capaciteit"; "battery.bank.metric.energy" = "Energie"; "battery.overview.empty.create" = "Accu toevoegen"; +"battery.onboarding.title" = "Voeg je eerste accu toe"; +"battery.onboarding.subtitle" = "Houd capaciteit en chemie van de accubank in de gaten om de gebruiksduur te beheersen."; "battery.bank.badge.voltage" = "Spanning"; "battery.bank.badge.capacity" = "Capaciteit"; "battery.bank.badge.energy" = "Energie";