Fix #26: Vorname im Gruß + Tour-Verbesserungen + UI-Feinschliff

- TodayView: Begrüßung zeigt Vornamen aus UserProfileStore
- TodayView: Leerer Zustand mit zwei CTA-Buttons (Moment / Todo)
- TodayView: Untertitel auf "Lass uns mit der Beziehungspflege starten." geändert
- Tour: Neue Schritte highlighten +Moment und +Todo in PersonDetailView
- Tour: PeopleListView navigiert automatisch zum ersten Kontakt beim Tour-Schritt
- Tour: App-Touren-Sektion in Einstellungen deaktiviert
- Schriftgrößen: Alle Überschriften um 2pt verkleinert (34→32, etc.)
- ContentView Preview: TourCoordinator-Environment ergänzt
- Lokalisierung: Neue Strings für Gruß, Leerzustand und Tour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 20:50:00 +02:00
parent 1e75f357ba
commit c647553eb7
15 changed files with 93 additions and 88 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ struct AppLockSetupView: View {
VStack(spacing: 8) {
Text(title)
.font(.system(size: 24, weight: .light, design: theme.displayDesign))
.font(.system(size: 22, weight: .light, design: theme.displayDesign))
.foregroundStyle(theme.contentPrimary)
Text(subtitle)
.font(.system(size: 15))
+2 -2
View File
@@ -23,7 +23,7 @@ struct AppLockView: View {
// Title
VStack(spacing: 6) {
Text("nahbar")
.font(.system(size: 34, weight: .light, design: theme.displayDesign))
.font(.system(size: 32, weight: .light, design: theme.displayDesign))
.foregroundStyle(theme.contentPrimary)
Text("Code eingeben")
.font(.system(size: 15))
@@ -166,7 +166,7 @@ struct PINPadView: View {
} else {
Button { onKey(.digit(key.first!)) } label: {
Text(key)
.font(.system(size: 28, weight: .light, design: theme.displayDesign))
.font(.system(size: 26, weight: .light, design: theme.displayDesign))
.foregroundStyle(theme.contentPrimary)
.frame(width: 80, height: 80)
.background(theme.surfaceCard)
+1 -1
View File
@@ -29,7 +29,7 @@ struct CallWindowSetupView: View {
.foregroundStyle(theme.accent)
Text("Gesprächszeit")
.font(.system(size: 26, weight: .light, design: theme.displayDesign))
.font(.system(size: 24, weight: .light, design: theme.displayDesign))
.foregroundStyle(theme.contentPrimary)
Text("nahbar erinnert dich täglich in deinem Zeitfenster und schlägt einen Kontakt vor — mit Notizen, damit du vorbereitet bist.")
+1 -2
View File
@@ -69,8 +69,6 @@ struct ContentView: View {
nahbarOnboardingDone = true
showingNahbarOnboarding = false
checkCallWindow()
// Tour nach Onboarding: Tab-Wechsel + Verzögerung nötig damit
// PeopleListView gerendert ist und anchorPreference-Frames gesammelt wurden.
scheduleTourIfNeeded()
}
}
@@ -397,4 +395,5 @@ struct ContentView: View {
.environmentObject(AppLockManager.shared)
.environmentObject(CloudSyncMonitor())
.environmentObject(UserProfileStore.shared)
.environment(TourCoordinator())
}
+1 -1
View File
@@ -95,7 +95,7 @@ struct IchView: View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("Ich")
.font(.system(size: 34, weight: .light, design: theme.displayDesign))
.font(.system(size: 32, weight: .light, design: theme.displayDesign))
.foregroundStyle(theme.contentPrimary)
Spacer()
Button { showingEdit = true } label: {
+42 -26
View File
@@ -230,6 +230,7 @@
},
"%lld Schritte" : {
"comment" : "SettingsView tour step count label (e.g. '6 Schritte')",
"extractionState" : "stale",
"localizations" : {
"en" : {
"variations" : {
@@ -917,6 +918,7 @@
},
"App-Touren" : {
"comment" : "SettingsView Tour section header",
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -2771,35 +2773,38 @@
}
}
},
"Guten Abend." : {
"comment" : "TodayView evening greeting",
"Guten Abend" : {
"comment" : "TodayView evening greeting (without punctuation, name appended in code)",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Good evening."
"value" : "Good evening"
}
}
}
},
"Guten Morgen" : {
"comment" : "TodayView morning greeting (without punctuation, name appended in code)",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Good morning"
}
}
}
},
"Guten Morgen." : {
"comment" : "TodayView morning greeting",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Good morning."
}
}
}
},
"Guten Tag." : {
"comment" : "TodayView afternoon greeting",
"Guten Tag" : {
"comment" : "TodayView afternoon greeting (without punctuation, name appended in code)",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Good afternoon."
"value" : "Good afternoon"
}
}
}
@@ -3372,8 +3377,20 @@
}
}
},
"Lass uns mit der Beziehungspflege starten." : {
"comment" : "TodayView empty state subtitle",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Let's start nurturing your relationships."
}
}
}
},
"Leg Vorhaben an und erhalte eine Erinnerung damit aus 'Wir müssen mal wieder…' ein echtes Treffen wird." : {
"comment" : "TourCatalog onboarding step 4 body",
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -3546,6 +3563,9 @@
}
}
}
},
"Mit '+ Todo' planst du konkrete Aufgaben für diese Person mit optionaler Erinnerung, damit nichts vergessen wird." : {
},
"Mittel" : {
"extractionState" : "stale",
@@ -4344,17 +4364,6 @@
}
}
},
"Oder einer, der es noch wird." : {
"comment" : "TodayView empty state subtitle",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Or one that will be."
}
}
}
},
"Offen" : {
"extractionState" : "stale",
"localizations" : {
@@ -5108,6 +5117,9 @@
}
}
}
},
"Tippe auf '+ Moment', um Treffen oder Gespräche festzuhalten so weißt du immer, worüber ihr das letzte Mal geredet habt." : {
},
"Tippe auf + um jemanden hinzuzufügen." : {
"comment" : "A description of how to add a new contact.",
@@ -5210,6 +5222,9 @@
}
}
}
},
"Todos anlegen" : {
},
"Touch ID aktiviert" : {
"comment" : "SettingsView biometric label when Touch ID is active",
@@ -5235,6 +5250,7 @@
},
"Tour starten" : {
"comment" : "SettingsView button to replay a tour",
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
+1 -1
View File
@@ -82,7 +82,7 @@ struct PaywallView: View {
.padding(.top, 24)
Text("nahbar")
.font(.system(size: 28, weight: .light, design: theme.displayDesign))
.font(.system(size: 26, weight: .light, design: theme.displayDesign))
.foregroundStyle(theme.contentPrimary)
Text("Wähle deinen Plan")
+10 -1
View File
@@ -4,6 +4,7 @@ import SwiftData
struct PeopleListView: View {
@Environment(\.nahbarTheme) var theme
@Environment(\.modelContext) var modelContext
@Environment(TourCoordinator.self) private var tourCoordinator
@Query(sort: \Person.name) private var people: [Person]
@StateObject private var store = StoreManager.shared
@@ -34,7 +35,7 @@ struct PeopleListView: View {
HStack(alignment: .firstTextBaseline) {
VStack(alignment: .leading, spacing: 2) {
Text("Menschen")
.font(.system(size: 34, weight: .light, design: theme.displayDesign))
.font(.system(size: 32, weight: .light, design: theme.displayDesign))
.foregroundStyle(theme.contentPrimary)
// Kontaktlimit-Hinweis für Free-Nutzer
if !store.isPro {
@@ -149,6 +150,14 @@ struct PeopleListView: View {
.sheet(isPresented: $showingPaywall) {
PaywallView(targeting: .pro)
}
// Automatisch zum ersten Kontakt navigieren, wenn die Tour
// den +Moment- oder +Todo-Button spotlighten möchte.
.onChange(of: tourCoordinator.currentStep?.target) { _, target in
guard target == .addMomentButton || target == .addTodoButton else { return }
if selectedPerson == nil, let first = filteredPeople.first {
selectedPerson = first
}
}
}
private var emptyState: some View {
+3 -1
View File
@@ -143,7 +143,7 @@ struct PersonDetailView: View {
VStack(alignment: .leading, spacing: 5) {
Text(person.name)
.font(.system(size: 26, weight: .light, design: theme.displayDesign))
.font(.system(size: 24, weight: .light, design: theme.displayDesign))
.foregroundStyle(theme.contentPrimary)
TagBadge(text: person.tag.rawValue)
@@ -180,6 +180,7 @@ struct PersonDetailView: View {
.background(theme.accent.opacity(0.10))
.clipShape(Capsule())
}
.tourTarget(.addMomentButton)
}
// Persönlichkeitsbasierte Vorhaben-Vorschläge (ersetzt nextStepSection)
@@ -465,6 +466,7 @@ struct PersonDetailView: View {
.background(theme.accent.opacity(0.10))
.clipShape(Capsule())
}
.tourTarget(.addTodoButton)
}
if visibleTodos.isEmpty {
+2 -37
View File
@@ -56,7 +56,7 @@ struct SettingsView: View {
// Header
Text("Einstellungen")
.font(.system(size: 34, weight: .light, design: theme.displayDesign))
.font(.system(size: 32, weight: .light, design: theme.displayDesign))
.foregroundStyle(theme.contentPrimary)
.padding(.horizontal, 20)
.padding(.top, 12)
@@ -591,42 +591,7 @@ struct SettingsView: View {
}
}
// App-Touren
VStack(alignment: .leading, spacing: 12) {
SectionHeader(title: "App-Touren", icon: "sparkles")
.padding(.horizontal, 20)
VStack(spacing: 0) {
ForEach(Array(TourCatalog.all.enumerated()), id: \.element.id) { index, tour in
if index > 0 { RowDivider() }
HStack(spacing: 14) {
Image(systemName: "sparkles")
.font(.system(size: 15))
.foregroundStyle(theme.contentTertiary)
.frame(width: 22)
VStack(alignment: .leading, spacing: 2) {
Text(tour.title)
.font(.system(size: 15))
.foregroundStyle(theme.contentPrimary)
Text(String.localizedStringWithFormat(String(localized: "%lld Schritte"), Int64(tour.steps.count)))
.font(.system(size: 12))
.foregroundStyle(theme.contentTertiary)
}
Spacer()
Button("Tour starten") {
tourCoordinator.start(tour.id)
}
.font(.system(size: 13, weight: .medium))
.foregroundStyle(theme.accent)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}
.background(theme.surfaceCard)
.clipShape(RoundedRectangle(cornerRadius: theme.radiusCard))
.padding(.horizontal, 20)
}
// App-Touren (deaktiviert)
// About
VStack(alignment: .leading, spacing: 12) {
+12 -6
View File
@@ -5,6 +5,7 @@ import UserNotifications
struct TodayView: View {
@Environment(\.nahbarTheme) var theme
@Environment(\.modelContext) private var modelContext
@EnvironmentObject private var profileStore: UserProfileStore
@Query private var people: [Person]
// V5: Nachwirkungen sind jetzt Treffen-Momente mit Status "warte_nachwirkung"
@Query(filter: #Predicate<Moment> {
@@ -102,11 +103,16 @@ struct TodayView: View {
}
}
private var greeting: LocalizedStringKey {
private var greeting: String {
let hour = Calendar.current.component(.hour, from: Date())
if hour < 12 { return "Guten Morgen." }
if hour < 18 { return "Guten Tag." }
return "Guten Abend."
let base: String
if hour < 12 { base = String(localized: "Guten Morgen") }
else if hour < 18 { base = String(localized: "Guten Tag") }
else { base = String(localized: "Guten Abend") }
let firstName = profileStore.name.split(separator: " ").first.map(String.init) ?? ""
if firstName.isEmpty { return "\(base)." }
return "\(base), \(firstName)."
}
private var formattedToday: String {
@@ -120,7 +126,7 @@ struct TodayView: View {
// Header
VStack(alignment: .leading, spacing: 4) {
Text(greeting)
.font(.system(size: 34, weight: .light, design: theme.displayDesign))
.font(.system(size: 32, weight: .light, design: theme.displayDesign))
.foregroundStyle(theme.contentPrimary)
Text(formattedToday)
.font(.system(size: 15, design: theme.displayDesign))
@@ -302,7 +308,7 @@ struct TodayView: View {
Text("Ein ruhiger Tag.")
.font(.system(size: 20, weight: .light, design: theme.displayDesign))
.foregroundStyle(theme.contentPrimary)
Text("Oder einer, der es noch wird.")
Text("Lass uns mit der Beziehungspflege starten.")
.font(.system(size: 15))
.foregroundStyle(theme.contentTertiary)
}
+1 -1
View File
@@ -31,7 +31,7 @@ struct Tour: Identifiable, Hashable {
triggerMode: TriggerMode
) {
precondition(!steps.isEmpty, "A tour must have at least 1 step.")
precondition(steps.count <= 6, "A tour must not exceed 6 steps. Got \(steps.count).")
precondition(steps.count <= 8, "A tour must not exceed 8 steps. Got \(steps.count).")
self.id = id
self.title = title
self.steps = steps
+9 -3
View File
@@ -33,9 +33,15 @@ enum TourCatalog {
),
TourStep(
title: "Plane das Nächste",
body: "Leg Vorhaben an und erhalte eine Erinnerung damit aus 'Wir müssen mal wieder…' ein echtes Treffen wird.",
target: nil,
preferredCardPosition: .center
body: "Tippe auf '+ Moment', um Treffen oder Gespräche festzuhalten so weißt du immer, worüber ihr das letzte Mal geredet habt.",
target: .addMomentButton,
preferredCardPosition: .below
),
TourStep(
title: "Todos anlegen",
body: "Mit '+ Todo' planst du konkrete Aufgaben für diese Person mit optionaler Erinnerung, damit nichts vergessen wird.",
target: .addTodoButton,
preferredCardPosition: .below
),
TourStep(
title: "Sanfte Erinnerungen",
+2
View File
@@ -8,6 +8,8 @@ enum TourTargetID: String, Hashable, CaseIterable {
case filterChips
case relationshipStrengthBadge
case contactCardFirst
case addMomentButton
case addTodoButton
case personalityTab
case insightsTab
case settingsEntry
@@ -7,11 +7,11 @@ import Testing
@Suite("TourCatalog Validierung")
struct TourCatalogTests {
@Test("Alle Touren haben mindestens 1 und höchstens 6 Steps")
@Test("Alle Touren haben mindestens 1 und höchstens 7 Steps")
func allToursHaveValidStepCount() {
for tour in TourCatalog.all {
#expect(!tour.steps.isEmpty, "Tour \(tour.id.rawValue) hat keine Steps")
#expect(tour.steps.count <= 6, "Tour \(tour.id.rawValue) hat mehr als 6 Steps")
#expect(tour.steps.count <= 7, "Tour \(tour.id.rawValue) hat mehr als 7 Steps")
}
}
@@ -25,9 +25,9 @@ struct TourCatalogTests {
}
}
@Test("Onboarding-Tour hat genau 6 Steps")
func onboardingTourHasSixSteps() {
#expect(TourCatalog.onboarding.steps.count == 6)
@Test("Onboarding-Tour hat genau 7 Steps")
func onboardingTourHasSevenSteps() {
#expect(TourCatalog.onboarding.steps.count == 7)
}
@Test("Onboarding-Tour hat triggerMode .manualOrFirstLaunch")