diff --git a/nahbar/nahbar/AppLockSetupView.swift b/nahbar/nahbar/AppLockSetupView.swift index 985ffb8..0d3d9d3 100644 --- a/nahbar/nahbar/AppLockSetupView.swift +++ b/nahbar/nahbar/AppLockSetupView.swift @@ -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)) diff --git a/nahbar/nahbar/AppLockView.swift b/nahbar/nahbar/AppLockView.swift index 4fa2221..ebd584c 100644 --- a/nahbar/nahbar/AppLockView.swift +++ b/nahbar/nahbar/AppLockView.swift @@ -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) diff --git a/nahbar/nahbar/CallWindowSetupView.swift b/nahbar/nahbar/CallWindowSetupView.swift index dcf52d3..a1ffd76 100644 --- a/nahbar/nahbar/CallWindowSetupView.swift +++ b/nahbar/nahbar/CallWindowSetupView.swift @@ -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.") diff --git a/nahbar/nahbar/ContentView.swift b/nahbar/nahbar/ContentView.swift index 1e8cffb..453f8e1 100644 --- a/nahbar/nahbar/ContentView.swift +++ b/nahbar/nahbar/ContentView.swift @@ -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()) } diff --git a/nahbar/nahbar/IchView.swift b/nahbar/nahbar/IchView.swift index 438735c..bf1783c 100644 --- a/nahbar/nahbar/IchView.swift +++ b/nahbar/nahbar/IchView.swift @@ -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: { diff --git a/nahbar/nahbar/Localizable.xcstrings b/nahbar/nahbar/Localizable.xcstrings index 924580a..32eb20c 100644 --- a/nahbar/nahbar/Localizable.xcstrings +++ b/nahbar/nahbar/Localizable.xcstrings @@ -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" : { diff --git a/nahbar/nahbar/PaywallView.swift b/nahbar/nahbar/PaywallView.swift index 9c637ef..2ce9ad3 100644 --- a/nahbar/nahbar/PaywallView.swift +++ b/nahbar/nahbar/PaywallView.swift @@ -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") diff --git a/nahbar/nahbar/PeopleListView.swift b/nahbar/nahbar/PeopleListView.swift index e2f74ac..f88bc7a 100644 --- a/nahbar/nahbar/PeopleListView.swift +++ b/nahbar/nahbar/PeopleListView.swift @@ -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 { diff --git a/nahbar/nahbar/PersonDetailView.swift b/nahbar/nahbar/PersonDetailView.swift index 1fdb789..185d963 100644 --- a/nahbar/nahbar/PersonDetailView.swift +++ b/nahbar/nahbar/PersonDetailView.swift @@ -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 { diff --git a/nahbar/nahbar/SettingsView.swift b/nahbar/nahbar/SettingsView.swift index 69720b7..a390271 100644 --- a/nahbar/nahbar/SettingsView.swift +++ b/nahbar/nahbar/SettingsView.swift @@ -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) { diff --git a/nahbar/nahbar/TodayView.swift b/nahbar/nahbar/TodayView.swift index afdceb9..1dfdaff 100644 --- a/nahbar/nahbar/TodayView.swift +++ b/nahbar/nahbar/TodayView.swift @@ -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 { @@ -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) } diff --git a/nahbar/nahbar/Tour/Tour.swift b/nahbar/nahbar/Tour/Tour.swift index 0b0464a..75b50bc 100644 --- a/nahbar/nahbar/Tour/Tour.swift +++ b/nahbar/nahbar/Tour/Tour.swift @@ -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 diff --git a/nahbar/nahbar/Tour/TourCatalog.swift b/nahbar/nahbar/Tour/TourCatalog.swift index 05ef68d..50d0e0c 100644 --- a/nahbar/nahbar/Tour/TourCatalog.swift +++ b/nahbar/nahbar/Tour/TourCatalog.swift @@ -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", diff --git a/nahbar/nahbar/Tour/TourTargetID.swift b/nahbar/nahbar/Tour/TourTargetID.swift index 7854d03..b6a4c5a 100644 --- a/nahbar/nahbar/Tour/TourTargetID.swift +++ b/nahbar/nahbar/Tour/TourTargetID.swift @@ -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 diff --git a/nahbar/nahbarTests/Tour/TourCatalogTests.swift b/nahbar/nahbarTests/Tour/TourCatalogTests.swift index 65e8d74..2647b02 100644 --- a/nahbar/nahbarTests/Tour/TourCatalogTests.swift +++ b/nahbar/nahbarTests/Tour/TourCatalogTests.swift @@ -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")