onboarding buttons in the system overview

This commit is contained in:
Stefan Lange-Hegermann
2025-10-22 17:17:57 +02:00
parent c7ff9322ef
commit 802b111aa7
10 changed files with 220 additions and 105 deletions

View File

@@ -77,6 +77,9 @@
"loads.overview.metric.count" = "Loads"; "loads.overview.metric.count" = "Loads";
"loads.overview.metric.current" = "Total Current"; "loads.overview.metric.current" = "Total Current";
"loads.overview.metric.power" = "Total Power"; "loads.overview.metric.power" = "Total Power";
"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.overview.status.missing_details.title" = "Missing load details"; "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.message" = "Enter cable length and wire size for %d %@ to see accurate recommendations.";
"loads.overview.status.missing_details.singular" = "load"; "loads.overview.status.missing_details.singular" = "load";
@@ -98,6 +101,7 @@
"battery.bank.metric.count" = "Batteries"; "battery.bank.metric.count" = "Batteries";
"battery.bank.metric.capacity" = "Capacity"; "battery.bank.metric.capacity" = "Capacity";
"battery.bank.metric.energy" = "Energy"; "battery.bank.metric.energy" = "Energy";
"battery.overview.empty.create" = "Add Battery";
"battery.bank.badge.voltage" = "Voltage"; "battery.bank.badge.voltage" = "Voltage";
"battery.bank.badge.capacity" = "Capacity"; "battery.bank.badge.capacity" = "Capacity";
"battery.bank.badge.energy" = "Energy"; "battery.bank.badge.energy" = "Energy";

View File

@@ -162,7 +162,6 @@ struct BatteryEditorView: View {
headerInfoBar headerInfoBar
List { List {
configurationSection configurationSection
summarySection
sliderSection sliderSection
} }
.listStyle(.plain) .listStyle(.plain)

View File

@@ -252,7 +252,10 @@ struct LoadsView: View {
loads: savedLoads, loads: savedLoads,
batteries: savedBatteries, batteries: savedBatteries,
onSelectLoads: { selectedComponentTab = .components }, onSelectLoads: { selectedComponentTab = .components },
onSelectBatteries: { selectedComponentTab = .batteries } onSelectBatteries: { selectedComponentTab = .batteries },
onCreateLoad: { createNewLoad() },
onBrowseLibrary: { showingComponentLibrary = true },
onCreateBattery: { startBatteryConfiguration() }
) )
.accessibilityIdentifier("system-overview") .accessibilityIdentifier("system-overview")
} }

View File

@@ -8,6 +8,9 @@ struct SystemOverviewView: View {
let batteries: [SavedBattery] let batteries: [SavedBattery]
let onSelectLoads: () -> Void let onSelectLoads: () -> Void
let onSelectBatteries: () -> Void let onSelectBatteries: () -> Void
let onCreateLoad: () -> Void
let onBrowseLibrary: () -> Void
let onCreateBattery: () -> Void
var body: some View { var body: some View {
ScrollView { ScrollView {
@@ -95,14 +98,9 @@ struct SystemOverviewView: View {
) )
} }
@ViewBuilder
private var loadsCard: some View { private var loadsCard: some View {
Button { if loads.isEmpty {
if suppressLoadNavigation {
suppressLoadNavigation = false
return
}
onSelectLoads()
} label: {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
HStack(alignment: .firstTextBaseline) { HStack(alignment: .firstTextBaseline) {
Text(loadsSummaryTitle) Text(loadsSummaryTitle)
@@ -110,15 +108,53 @@ struct SystemOverviewView: View {
Spacer() Spacer()
} }
if loads.isEmpty { VStack(alignment: .leading, spacing: 8) {
VStack(alignment: .leading, spacing: 8) { Text(loadsEmptyTitle)
Text(loadsEmptyTitle) .font(.subheadline.weight(.semibold))
.font(.subheadline.weight(.semibold)) Text(loadsEmptyMessage)
Text(loadsEmptySubtitle) .font(.footnote)
.font(.footnote) .foregroundStyle(.secondary)
.foregroundStyle(.secondary) }
VStack(alignment: .leading, spacing: 10) {
Button(action: onCreateLoad) {
Label(loadsEmptyCreateAction, systemImage: "plus")
.frame(maxWidth: .infinity)
} }
} else { .buttonStyle(.borderedProminent)
.controlSize(.large)
Button(action: onBrowseLibrary) {
Label(loadsEmptyBrowseAction, systemImage: "books.vertical")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.tint(.accentColor)
.controlSize(.large)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 18)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(Color(.systemBackground))
)
} else {
Button {
if suppressLoadNavigation {
suppressLoadNavigation = false
return
}
onSelectLoads()
} label: {
VStack(alignment: .leading, spacing: 16) {
HStack(alignment: .firstTextBaseline) {
Text(loadsSummaryTitle)
.font(.headline.weight(.semibold))
Spacer()
}
ViewThatFits(in: .horizontal) { ViewThatFits(in: .horizontal) {
HStack(spacing: 16) { HStack(spacing: 16) {
summaryMetric( summaryMetric(
@@ -173,32 +209,32 @@ struct SystemOverviewView: View {
) )
} }
} }
.padding(.horizontal, 16)
.padding(.vertical, 18)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(Color(.systemBackground))
)
} }
.padding(.horizontal, 16) .buttonStyle(.plain)
.padding(.vertical, 18) .alert(item: $activeStatus) { status in
.frame(maxWidth: .infinity, alignment: .leading) let detail = status.detailInfo()
.background( return Alert(
RoundedRectangle(cornerRadius: 20, style: .continuous) title: Text(detail.title),
.fill(Color(.systemBackground)) message: Text(detail.message),
) dismissButton: .default(
} Text(
.buttonStyle(.plain) NSLocalizedString(
.alert(item: $activeStatus) { status in "battery.bank.status.dismiss",
let detail = status.detailInfo() bundle: .main,
return Alert( value: "Got it",
title: Text(detail.title), comment: "Dismiss button title for load status alert"
message: Text(detail.message), )
dismissButton: .default(
Text(
NSLocalizedString(
"battery.bank.status.dismiss",
bundle: .main,
value: "Got it",
comment: "Dismiss button title for load status alert"
) )
) )
) )
) }
} }
} }
@@ -210,78 +246,87 @@ struct SystemOverviewView: View {
.font(.headline.weight(.semibold)) .font(.headline.weight(.semibold))
if let warning = batteryWarning { if let warning = batteryWarning {
HStack(spacing: 6) { HStack(spacing: 6) {
Image(systemName: warning.symbol) Image(systemName: warning.symbol)
.font(.system(size: 14, weight: .semibold)) .font(.system(size: 14, weight: .semibold))
.foregroundStyle(warning.tint) .foregroundStyle(warning.tint)
Text(warning.shortLabel) Text(warning.shortLabel)
.font(.caption.weight(.semibold)) .font(.caption.weight(.semibold))
.foregroundStyle(warning.tint) .foregroundStyle(warning.tint)
} }
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 6) .padding(.vertical, 6)
.background( .background(
RoundedRectangle(cornerRadius: 12, style: .continuous) RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(warning.tint.opacity(0.12)) .fill(warning.tint.opacity(0.12))
)
}
Spacer()
}
if batteries.isEmpty {
VStack(alignment: .leading, spacing: 8) {
Text(batteryEmptyTitle)
.font(.subheadline.weight(.semibold))
Text(batteryEmptySubtitle)
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
} else {
ViewThatFits(in: .horizontal) {
HStack(spacing: 16) {
summaryMetric(
icon: "battery.100",
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .blue
)
summaryMetric(
icon: "gauge.medium",
label: batteryCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
)
summaryMetric(
icon: "bolt.circle",
label: batteryEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
) )
} }
Spacer()
}
if batteries.isEmpty {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
summaryMetric( VStack(alignment: .leading, spacing: 4) {
icon: "battery.100", Text(batteryEmptyTitle)
label: batteryCountLabel, .font(.subheadline.weight(.semibold))
value: "\(batteries.count)", Text(batteryEmptySubtitle)
tint: .blue .font(.footnote)
) .foregroundStyle(.secondary)
summaryMetric( .fixedSize(horizontal: false, vertical: true)
icon: "gauge.medium", }
label: batteryCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"), Button(action: onCreateBattery) {
tint: .orange Label(batteryEmptyCreateAction, systemImage: "plus")
) .frame(maxWidth: .infinity)
summaryMetric( }
icon: "bolt.circle", .buttonStyle(.borderedProminent)
label: batteryEnergyLabel, .controlSize(.large)
value: formattedValue(totalEnergy, unit: "Wh"), }
tint: .green } else {
) ViewThatFits(in: .horizontal) {
HStack(spacing: 16) {
summaryMetric(
icon: "battery.100",
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .blue
)
summaryMetric(
icon: "gauge.medium",
label: batteryCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
)
summaryMetric(
icon: "bolt.circle",
label: batteryEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
)
}
VStack(alignment: .leading, spacing: 12) {
summaryMetric(
icon: "battery.100",
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .blue
)
summaryMetric(
icon: "gauge.medium",
label: batteryCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
)
summaryMetric(
icon: "bolt.circle",
label: batteryEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
)
}
} }
} }
} }
}
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 18) .padding(.vertical, 18)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
@@ -540,6 +585,33 @@ struct SystemOverviewView: View {
) )
} }
private var loadsEmptyMessage: String {
NSLocalizedString(
"loads.overview.empty.message",
bundle: .main,
value: "Start by adding a load to see system insights.",
comment: "Message shown when no loads exist"
)
}
private var loadsEmptyCreateAction: String {
NSLocalizedString(
"loads.overview.empty.create",
bundle: .main,
value: "Create Load",
comment: "Button title to create a new load"
)
}
private var loadsEmptyBrowseAction: String {
NSLocalizedString(
"loads.overview.empty.library",
bundle: .main,
value: "Browse Library",
comment: "Button title to open load library"
)
}
private var batterySummaryTitle: String { private var batterySummaryTitle: String {
NSLocalizedString( NSLocalizedString(
"battery.bank.header.title", "battery.bank.header.title",
@@ -596,6 +668,15 @@ struct SystemOverviewView: View {
return String(format: format, system.name) return String(format: format, system.name)
} }
private var batteryEmptyCreateAction: String {
NSLocalizedString(
"battery.overview.empty.create",
bundle: .main,
value: "Create Battery",
comment: "Button title to create a new battery"
)
}
private var systemOverviewTitle: String { private var systemOverviewTitle: String {
NSLocalizedString( NSLocalizedString(
"overview.system.header.title", "overview.system.header.title",

View File

@@ -17,6 +17,7 @@ struct SystemsView: View {
@State private var systemNavigationTarget: SystemNavigationTarget? @State private var systemNavigationTarget: SystemNavigationTarget?
@State private var showingComponentLibrary = false @State private var showingComponentLibrary = false
@State private var showingSettings = false @State private var showingSettings = false
@State private var hasPerformedInitialAutoNavigation = false
private let systemColorOptions = [ private let systemColorOptions = [
"blue", "green", "orange", "red", "purple", "yellow", "blue", "green", "orange", "red", "purple", "yellow",
@@ -140,6 +141,9 @@ struct SystemsView: View {
) )
} }
} }
.onAppear {
performInitialAutoNavigationIfNeeded()
}
.sheet(isPresented: $showingComponentLibrary) { .sheet(isPresented: $showingComponentLibrary) {
ComponentLibraryView { item in ComponentLibraryView { item in
addComponentFromLibrary(item) addComponentFromLibrary(item)
@@ -219,6 +223,14 @@ struct SystemsView: View {
return newSystem return newSystem
} }
private func performInitialAutoNavigationIfNeeded() {
guard !hasPerformedInitialAutoNavigation else { return }
hasPerformedInitialAutoNavigation = true
guard systems.count == 1, let system = systems.first else { return }
navigateToSystem(system, presentSystemEditor: false, loadToOpen: nil, animated: false)
}
private func addComponentFromLibrary(_ item: ComponentLibraryItem) { private func addComponentFromLibrary(_ item: ComponentLibraryItem) {
let system = makeSystem() let system = makeSystem()
let load = createLoad(from: item, in: system) let load = createLoad(from: item, in: system)

View File

@@ -145,6 +145,9 @@
"loads.overview.metric.count" = "Verbraucher"; "loads.overview.metric.count" = "Verbraucher";
"loads.overview.metric.current" = "Strom"; "loads.overview.metric.current" = "Strom";
"loads.overview.metric.power" = "Leistung"; "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.overview.status.missing_details.title" = "Fehlende Verbraucherdetails"; "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.message" = "Gib Kabellänge und Leitungsquerschnitt für %d %@ ein, um genaue Empfehlungen zu erhalten.";
"loads.overview.status.missing_details.singular" = "Verbraucher"; "loads.overview.status.missing_details.singular" = "Verbraucher";
@@ -166,6 +169,7 @@
"battery.bank.metric.count" = "Batterien"; "battery.bank.metric.count" = "Batterien";
"battery.bank.metric.capacity" = "Kapazität"; "battery.bank.metric.capacity" = "Kapazität";
"battery.bank.metric.energy" = "Energie"; "battery.bank.metric.energy" = "Energie";
"battery.overview.empty.create" = "Batterie hinzufügen";
"battery.bank.badge.voltage" = "Spannung"; "battery.bank.badge.voltage" = "Spannung";
"battery.bank.badge.capacity" = "Kapazität"; "battery.bank.badge.capacity" = "Kapazität";
"battery.bank.badge.energy" = "Energie"; "battery.bank.badge.energy" = "Energie";

View File

@@ -144,6 +144,9 @@
"loads.overview.metric.count" = "Cargas"; "loads.overview.metric.count" = "Cargas";
"loads.overview.metric.current" = "Corriente total"; "loads.overview.metric.current" = "Corriente total";
"loads.overview.metric.power" = "Potencia total"; "loads.overview.metric.power" = "Potencia total";
"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.overview.status.missing_details.title" = "Faltan detalles de la carga"; "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.message" = "Introduce la longitud del cable y el calibre del conductor para %d %@ para obtener recomendaciones precisas.";
"loads.overview.status.missing_details.singular" = "carga"; "loads.overview.status.missing_details.singular" = "carga";
@@ -165,6 +168,7 @@
"battery.bank.metric.count" = "Baterías"; "battery.bank.metric.count" = "Baterías";
"battery.bank.metric.capacity" = "Capacidad"; "battery.bank.metric.capacity" = "Capacidad";
"battery.bank.metric.energy" = "Energía"; "battery.bank.metric.energy" = "Energía";
"battery.overview.empty.create" = "Añadir batería";
"battery.bank.badge.voltage" = "Voltaje"; "battery.bank.badge.voltage" = "Voltaje";
"battery.bank.badge.capacity" = "Capacidad"; "battery.bank.badge.capacity" = "Capacidad";
"battery.bank.badge.energy" = "Energía"; "battery.bank.badge.energy" = "Energía";

View File

@@ -144,6 +144,9 @@
"loads.overview.metric.count" = "Charges"; "loads.overview.metric.count" = "Charges";
"loads.overview.metric.current" = "Courant total"; "loads.overview.metric.current" = "Courant total";
"loads.overview.metric.power" = "Puissance totale"; "loads.overview.metric.power" = "Puissance totale";
"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.overview.status.missing_details.title" = "Détails de charge manquants"; "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.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"; "loads.overview.status.missing_details.singular" = "charge";
@@ -165,6 +168,7 @@
"battery.bank.metric.count" = "Batteries"; "battery.bank.metric.count" = "Batteries";
"battery.bank.metric.capacity" = "Capacité"; "battery.bank.metric.capacity" = "Capacité";
"battery.bank.metric.energy" = "Énergie"; "battery.bank.metric.energy" = "Énergie";
"battery.overview.empty.create" = "Ajouter une batterie";
"battery.bank.badge.voltage" = "Tension"; "battery.bank.badge.voltage" = "Tension";
"battery.bank.badge.capacity" = "Capacité"; "battery.bank.badge.capacity" = "Capacité";
"battery.bank.badge.energy" = "Énergie"; "battery.bank.badge.energy" = "Énergie";

View File

@@ -144,6 +144,9 @@
"loads.overview.metric.count" = "Lasten"; "loads.overview.metric.count" = "Lasten";
"loads.overview.metric.current" = "Totale stroom"; "loads.overview.metric.current" = "Totale stroom";
"loads.overview.metric.power" = "Totaal vermogen"; "loads.overview.metric.power" = "Totaal vermogen";
"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.overview.status.missing_details.title" = "Ontbrekende lastdetails"; "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.message" = "Voer kabellengte en kabeldoorsnede in voor %d %@ om nauwkeurige aanbevelingen te krijgen.";
"loads.overview.status.missing_details.singular" = "last"; "loads.overview.status.missing_details.singular" = "last";
@@ -165,6 +168,7 @@
"battery.bank.metric.count" = "Batterijen"; "battery.bank.metric.count" = "Batterijen";
"battery.bank.metric.capacity" = "Capaciteit"; "battery.bank.metric.capacity" = "Capaciteit";
"battery.bank.metric.energy" = "Energie"; "battery.bank.metric.energy" = "Energie";
"battery.overview.empty.create" = "Accu toevoegen";
"battery.bank.badge.voltage" = "Spanning"; "battery.bank.badge.voltage" = "Spanning";
"battery.bank.badge.capacity" = "Capaciteit"; "battery.bank.badge.capacity" = "Capaciteit";
"battery.bank.badge.energy" = "Energie"; "battery.bank.badge.energy" = "Energie";

View File

@@ -4,4 +4,4 @@ LoadEditorView=Berechne*zuverlässig*\ndie richtige Sicherung
ComponentSelectorView=Finde im*großen*Teilekatalog\nwas du suchst ComponentSelectorView=Finde im*großen*Teilekatalog\nwas du suchst
SystemsWithSampleData=Navigiere*schnell*\ndurch Deine Systeme SystemsWithSampleData=Navigiere*schnell*\ndurch Deine Systeme
AdventureVanLoads=Erstelle*individuelle*\nVerbraucher für Dein System AdventureVanLoads=Erstelle*individuelle*\nVerbraucher für Dein System
AdventureVanBillOfMaterials=Behalte den*Überblick*\nwas du schon gekauft hast AdventureVanBillOfMaterials=Behalte den*Überblick*\nwelche Teile du schon hast