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:
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user