diff --git a/Andromida/App/Localization/Localizable.xcstrings b/Andromida/App/Localization/Localizable.xcstrings index 925cd39..c7968e4 100644 --- a/Andromida/App/Localization/Localizable.xcstrings +++ b/Andromida/App/Localization/Localizable.xcstrings @@ -352,6 +352,9 @@ }, "All habits complete! Great work today." : { + }, + "All-time habits completed" : { + "comment" : "Caption for the Total Check-ins insight card." }, "Always visible" : { "comment" : "Combined display name with time range for rituals that can be performed at any time.", @@ -400,21 +403,12 @@ "comment" : "Label text for a badge indicating that their habit completion rate is below the average for the week.", "isCommentAutoGenerated" : true }, - "Best Streak" : { - "comment" : "Title for a stat card displaying the longest streak of days in an arc.", - "isCommentAutoGenerated" : true - }, "Best Ritual" : { "comment" : "Title for an insight card showing the highest-performing ritual by completion rate." }, - "Your highest-performing ritual by completion rate in the current arc. Keep it up!" : { - "comment" : "Explanation for the Best Ritual insight card." - }, - "No active rituals" : { - "comment" : "Caption for the Best Ritual insight card when there are no active rituals." - }, - "Start a ritual to see which one you complete most consistently." : { - "comment" : "Explanation for the Best Ritual insight card when there are no active rituals." + "Best Streak" : { + "comment" : "Title for a stat card displaying the longest streak of days in an arc.", + "isCommentAutoGenerated" : true }, "Body scan for tension" : { "comment" : "Habit title for a mindfulness ritual that involves scanning the body for tension.", @@ -1383,6 +1377,9 @@ "Later" : { "comment" : "A button that dismisses the renewal prompt and returns to the main screen.", "isCommentAutoGenerated" : true + }, + "Less" : { + }, "Let go of the day" : { "comment" : "Habit title for a mindfulness ritual where the user lets go of their worries or stresses of the day.", @@ -1449,6 +1446,7 @@ }, "Momentum at a glance" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1470,6 +1468,10 @@ } } }, + "More" : { + "comment" : "The text for a button that expands or collapses a list.", + "isCommentAutoGenerated" : true + }, "Morning" : { "comment" : "Name of the time of day option for a ritual that appears in the Today view in the morning.", "isCommentAutoGenerated" : true @@ -1585,6 +1587,9 @@ "comment" : "A description shown when a ritual does not have an active arc.", "isCommentAutoGenerated" : true }, + "No active rituals" : { + "comment" : "Caption for the Best Ritual insight card when there are no active rituals." + }, "No Active Rituals" : { "comment" : "A message displayed when a user has no active rituals.", "isCommentAutoGenerated" : true @@ -2150,14 +2155,6 @@ "comment" : "Notes for the \"Gratitude Practice\" ritual preset.", "isCommentAutoGenerated" : true }, - "Show less" : { - "comment" : "A button label that indicates to collapse the history view.", - "isCommentAutoGenerated" : true - }, - "Show more" : { - "comment" : "A button label that indicates more content is available.", - "isCommentAutoGenerated" : true - }, "Shows rituals for the current time of day. Check in here daily." : { }, @@ -2233,6 +2230,9 @@ "comment" : "A button that starts a ritual from a goal category. The argument is the name of the goal.", "isCommentAutoGenerated" : true }, + "Start a ritual to see which one you complete most consistently." : { + "comment" : "Explanation for the Best Ritual insight card when there are no active rituals." + }, "Start building better habits" : { "comment" : "Subtitle for the \"No Active Rituals\" section in the \"Today\" view when there are no active rituals.", "isCommentAutoGenerated" : true @@ -2394,6 +2394,9 @@ "comment" : "Explanation of the value for the Insight Card titled \"Habits today\".", "isCommentAutoGenerated" : true }, + "The total number of habit check-ins you've made since you started using Rituals. Every check-in counts toward building lasting change." : { + "comment" : "Explanation for the Total Check-ins insight card." + }, "Theme or tagline" : { "comment" : "A label for an optional tagline or theme associated with a ritual.", "isCommentAutoGenerated" : true @@ -2487,12 +2490,6 @@ "Total Check-ins" : { "comment" : "Title for an insight card showing the total number of habits completed all-time." }, - "All-time habits completed" : { - "comment" : "Caption for the Total Check-ins insight card." - }, - "The total number of habit check-ins you've made since you started using Rituals. Every check-in counts toward building lasting change." : { - "comment" : "Explanation for the Total Check-ins insight card." - }, "Track your streaks, progress, and trends over time." : { "comment" : "Description of a feature card in the \"WhatsNextStepView\" that explains how to use the app's insights feature.", "isCommentAutoGenerated" : true @@ -2536,6 +2533,9 @@ "comment" : "Description of a feature card in the \"WhatsNextStepView\" that explains how to manage all your rituals, regardless of the time they were created.", "isCommentAutoGenerated" : true }, + "Weekly average" : { + "comment" : "Caption for the 7-Day Avg insight card." + }, "Weekly completion chart" : { "comment" : "An accessibility label for the weekly completion chart in the insight detail sheet.", "isCommentAutoGenerated" : true @@ -2544,12 +2544,6 @@ "comment" : "Name of a ritual preset that users can add to their collection, focusing on preparing for a fresh week.", "isCommentAutoGenerated" : true }, - "Weekly average" : { - "comment" : "Caption for the 7-Day Avg insight card." - }, - "Your average completion rate over the last 7 days. This smooths out daily fluctuations and shows your typical consistency." : { - "comment" : "Explanation for the 7-Day Avg insight card." - }, "Welcome to Rituals" : { "comment" : "The title of the welcome screen in the setup wizard.", "isCommentAutoGenerated" : true @@ -2662,6 +2656,9 @@ "comment" : "Tip provided when the user is at their longest streak and it is greater than zero.", "isCommentAutoGenerated" : true }, + "Your average completion rate over the last 7 days. This smooths out daily fluctuations and shows your typical consistency." : { + "comment" : "Explanation for the 7-Day Avg insight card." + }, "Your completion percentage for today across all rituals. The chart shows your last 7 days—this helps you spot patterns and stay consistent." : { "comment" : "Explanation of the insight card that shows the user their completion percentage for today across all their active rituals, with a chart displaying their last 7 days.", "isCommentAutoGenerated" : true @@ -2697,14 +2694,13 @@ } } }, + "Your highest-performing ritual by completion rate in the current arc. Keep it up!" : { + "comment" : "Explanation for the Best Ritual insight card." + }, "Your Journey" : { "comment" : "A heading for the summary of a user's ritual progress.", "isCommentAutoGenerated" : true }, - "Your journey over time" : { - "comment" : "Subtitle for the History view, describing what the view is about.", - "isCommentAutoGenerated" : true - }, "Your next chapter" : { "localizations" : { "en" : { diff --git a/Andromida/App/Views/History/HistoryView.swift b/Andromida/App/Views/History/HistoryView.swift index e32484a..c73c50f 100644 --- a/Andromida/App/Views/History/HistoryView.swift +++ b/Andromida/App/Views/History/HistoryView.swift @@ -23,7 +23,6 @@ struct HistoryView: View { @State private var monthsToShow = 2 @State private var refreshToken = UUID() @State private var cachedProgressByDate: [Date: Double] = [:] - @State private var headerMinY: CGFloat = 0 private let calendar = Calendar.current private let baseMonthsToShow = 2 @@ -44,29 +43,6 @@ struct HistoryView: View { ) } - private let headerMaxHeight: CGFloat = 84 - private let headerMinHeight: CGFloat = 58 - - private var headerHeight: CGFloat { - let collapseDistance = headerMaxHeight - headerMinHeight - let collapseAmount = min(max(-headerMinY, 0), collapseDistance) - return headerMaxHeight - collapseAmount - } - - private var headerScale: CGFloat { - let collapseDistance = headerMaxHeight - headerMinHeight - guard collapseDistance > 0 else { return 1 } - let progress = min(max(-headerMinY / collapseDistance, 0), 1) - return 1 - (0.08 * progress) - } - - private var headerOpacity: Double { - let collapseDistance = headerMaxHeight - headerMinHeight - guard collapseDistance > 0 else { return 1 } - let progress = min(max(-headerMinY / collapseDistance, 0), 1) - return 1 - (0.25 * progress) - } - /// Generate months based on expanded state /// - Collapsed: Last month + current month (2 months) /// - Expanded: Up to 12 months of history @@ -106,56 +82,57 @@ struct HistoryView: View { var body: some View { ScrollView(.vertical, showsIndicators: false) { - LazyVStack(alignment: .leading, spacing: Design.Spacing.large, pinnedViews: [.sectionHeaders]) { - Section( - header: headerSection - .padding(.top, Design.Spacing.large) - .padding(.bottom, Design.Spacing.small) - .frame(maxWidth: .infinity, alignment: .leading) - .frame(height: headerHeight, alignment: .bottom) - .scaleEffect(headerScale, anchor: .leading) - .opacity(headerOpacity) - .background( - GeometryReader { proxy in - Color.clear - .preference(key: HistoryHeaderMinYKey.self, value: proxy.frame(in: .named("historyScroll")).minY) + VStack(alignment: .leading, spacing: Design.Spacing.large) { + // Ritual filter picker + ritualPicker + + // Month calendars - 2-column grid on iPad/landscape + LazyVGrid(columns: monthColumns, alignment: .leading, spacing: Design.Spacing.large) { + ForEach(months, id: \.self) { month in + HistoryMonthView( + month: month, + selectedRitual: selectedRitual, + completionRate: { date, ritual in + let day = calendar.startOfDay(for: date) + return cachedProgressByDate[day] ?? store.completionRate(for: day, ritual: ritual) + }, + onDayTapped: { date in + selectedDateItem = IdentifiableDate(date: date) } ) - .background(AppSurface.primary.opacity(0.9)) - ) { - // Ritual filter picker - ritualPicker - - // Month calendars - 2-column grid on iPad/landscape - LazyVGrid(columns: monthColumns, alignment: .leading, spacing: Design.Spacing.large) { - ForEach(months, id: \.self) { month in - HistoryMonthView( - month: month, - selectedRitual: selectedRitual, - completionRate: { date, ritual in - let day = calendar.startOfDay(for: date) - return cachedProgressByDate[day] ?? store.completionRate(for: day, ritual: ritual) - }, - onDayTapped: { date in - selectedDateItem = IdentifiableDate(date: date) - } - ) - .frame(maxHeight: .infinity, alignment: .top) - } + .frame(maxHeight: .infinity, alignment: .top) } - .id(refreshToken) } + .id(refreshToken) } - .padding(.horizontal, Design.Spacing.large) - .padding(.bottom, Design.Spacing.large) + .padding(Design.Spacing.large) } - .coordinateSpace(name: "historyScroll") - .onPreferenceChange(HistoryHeaderMinYKey.self) { headerMinY = $0 } .background(LinearGradient( colors: [AppSurface.primary, AppSurface.secondary], startPoint: .topLeading, endPoint: .bottomTrailing )) + .navigationTitle(String(localized: "History")) + .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItem(placement: .primaryAction) { + if hasMoreHistory || monthsToShow > baseMonthsToShow { + Button { + withAnimation(.easeInOut(duration: Design.Animation.standard)) { + if monthsToShow > baseMonthsToShow { + monthsToShow = baseMonthsToShow + } else { + let totalAvailableMonths = totalMonthsAvailable(from: Date()) + monthsToShow = min(monthsToShow + monthChunkSize, totalAvailableMonths) + } + } + } label: { + Text(monthsToShow > baseMonthsToShow ? String(localized: "Less") : String(localized: "More")) + .foregroundStyle(AppAccent.primary) + } + } + } + } .onChange(of: store.rituals) { _, newRituals in if let selectedRitual { self.selectedRitual = newRituals.first { $0.id == selectedRitual.id } @@ -183,39 +160,6 @@ struct HistoryView: View { } } - private var headerSection: some View { - HStack(alignment: .top) { - SectionHeaderView( - title: String(localized: "History"), - subtitle: String(localized: "Your journey over time") - ) - - Spacer() - - if hasMoreHistory || monthsToShow > baseMonthsToShow { - Button { - withAnimation(.easeInOut(duration: Design.Animation.standard)) { - if monthsToShow > baseMonthsToShow { - monthsToShow = baseMonthsToShow - } else { - let totalAvailableMonths = totalMonthsAvailable(from: Date()) - monthsToShow = min(monthsToShow + monthChunkSize, totalAvailableMonths) - } - } - } label: { - HStack(spacing: Design.Spacing.xSmall) { - Text(monthsToShow > baseMonthsToShow ? String(localized: "Show less") : String(localized: "Show more")) - .font(.subheadline) - Image(systemName: monthsToShow > baseMonthsToShow ? "chevron.up" : "chevron.down") - .font(.caption) - } - .foregroundStyle(AppAccent.primary) - } - .buttonStyle(.plain) - } - } - } - private var ritualPicker: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: Design.Spacing.small) { @@ -288,14 +232,6 @@ struct HistoryView: View { } } -private struct HistoryHeaderMinYKey: PreferenceKey { - static var defaultValue: CGFloat = 0 - - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value = nextValue() - } -} - #Preview { HistoryView(store: RitualStore.preview) } diff --git a/Andromida/App/Views/Insights/InsightsView.swift b/Andromida/App/Views/Insights/InsightsView.swift index 35c6235..a4c3ba2 100644 --- a/Andromida/App/Views/Insights/InsightsView.swift +++ b/Andromida/App/Views/Insights/InsightsView.swift @@ -29,11 +29,6 @@ struct InsightsView: View { NavigationStack { ScrollView(.vertical, showsIndicators: false) { VStack(alignment: .leading, spacing: Design.Spacing.large) { - SectionHeaderView( - title: String(localized: "Insights"), - subtitle: String(localized: "Momentum at a glance") - ) - // Grid with drag-and-drop support in edit mode LazyVGrid(columns: columns, spacing: Design.Spacing.medium) { ForEach(orderedCards) { card in @@ -56,7 +51,7 @@ struct InsightsView: View { endPoint: .bottomTrailing )) .navigationTitle(String(localized: "Insights")) - .navigationBarTitleDisplayMode(.inline) + .navigationBarTitleDisplayMode(.large) .toolbar { ToolbarItem(placement: .primaryAction) { Button { diff --git a/Andromida/App/Views/Rituals/RitualsView.swift b/Andromida/App/Views/Rituals/RitualsView.swift index ab51bfb..cdc55aa 100644 --- a/Andromida/App/Views/Rituals/RitualsView.swift +++ b/Andromida/App/Views/Rituals/RitualsView.swift @@ -63,7 +63,7 @@ struct RitualsView: View { endPoint: .bottomTrailing )) .navigationTitle(String(localized: "Rituals")) - .navigationBarTitleDisplayMode(.inline) + .navigationBarTitleDisplayMode(.large) .toolbar { ToolbarItem(placement: .primaryAction) { Menu { diff --git a/Andromida/App/Views/Settings/SettingsView.swift b/Andromida/App/Views/Settings/SettingsView.swift index 091ca57..7956a76 100644 --- a/Andromida/App/Views/Settings/SettingsView.swift +++ b/Andromida/App/Views/Settings/SettingsView.swift @@ -167,7 +167,7 @@ struct SettingsView: View { } .background(AppSurface.primary) .navigationTitle(String(localized: "Settings")) - .navigationBarTitleDisplayMode(.inline) + .navigationBarTitleDisplayMode(.large) } } diff --git a/Andromida/App/Views/Today/TodayView.swift b/Andromida/App/Views/Today/TodayView.swift index e24299e..29d7e91 100644 --- a/Andromida/App/Views/Today/TodayView.swift +++ b/Andromida/App/Views/Today/TodayView.swift @@ -5,7 +5,6 @@ struct TodayView: View { @Bindable var store: RitualStore @Bindable var categoryStore: CategoryStore @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @State private var headerMinY: CGFloat = 0 /// Rituals to show now based on current time of day private var todayRituals: [Ritual] { @@ -36,88 +35,47 @@ struct TodayView: View { horizontalSizeClass: horizontalSizeClass ) } - - private let headerMaxHeight: CGFloat = 76 - private let headerMinHeight: CGFloat = 52 - - private var headerHeight: CGFloat { - let collapseDistance = headerMaxHeight - headerMinHeight - let collapseAmount = min(max(-headerMinY, 0), collapseDistance) - return headerMaxHeight - collapseAmount - } - - private var headerScale: CGFloat { - let collapseDistance = headerMaxHeight - headerMinHeight - guard collapseDistance > 0 else { return 1 } - let progress = min(max(-headerMinY / collapseDistance, 0), 1) - return 1 - (0.08 * progress) - } - - private var headerOpacity: Double { - let collapseDistance = headerMaxHeight - headerMinHeight - guard collapseDistance > 0 else { return 1 } - let progress = min(max(-headerMinY / collapseDistance, 0), 1) - return 1 - (0.25 * progress) - } var body: some View { ScrollView(.vertical, showsIndicators: false) { - LazyVStack(alignment: .leading, spacing: Design.Spacing.large, pinnedViews: [.sectionHeaders]) { - Section( - header: TodayHeaderView(dateText: store.todayDisplayString) - .padding(.top, Design.Spacing.large) - .padding(.bottom, Design.Spacing.small) - .frame(maxWidth: .infinity, alignment: .leading) - .frame(height: headerHeight, alignment: .bottom) - .scaleEffect(headerScale, anchor: .leading) - .opacity(headerOpacity) - .background( - GeometryReader { proxy in - Color.clear - .preference(key: TodayHeaderMinYKey.self, value: proxy.frame(in: .named("todayScroll")).minY) - } - ) - .background(AppSurface.primary.opacity(0.9)) - ) { - if todayRituals.isEmpty { - if hasRitualsButNotNow { - // Has active rituals but none for current time of day - TodayNoRitualsForTimeView(store: store) - } else { - // No active rituals at all - TodayEmptyStateView(store: store, categoryStore: categoryStore) - } + VStack(alignment: .leading, spacing: Design.Spacing.large) { + if todayRituals.isEmpty { + if hasRitualsButNotNow { + // Has active rituals but none for current time of day + TodayNoRitualsForTimeView(store: store) } else { - // Use 2-column grid on iPad/landscape when multiple rituals - LazyVGrid(columns: ritualColumns, alignment: .leading, spacing: Design.Spacing.large) { - ForEach(todayRituals) { ritual in - TodayRitualSectionView( - focusTitle: ritual.title, - focusTheme: ritual.theme, - dayLabel: store.ritualDayLabel(for: ritual), - completionSummary: store.completionSummary(for: ritual), - progress: store.ritualProgress(for: ritual), - habitRows: habitRows(for: ritual), - iconName: ritual.iconName, - timeOfDay: ritual.timeOfDay - ) - .frame(maxHeight: .infinity, alignment: .top) - } + // No active rituals at all + TodayEmptyStateView(store: store, categoryStore: categoryStore) + } + } else { + // Use 2-column grid on iPad/landscape when multiple rituals + LazyVGrid(columns: ritualColumns, alignment: .leading, spacing: Design.Spacing.large) { + ForEach(todayRituals) { ritual in + TodayRitualSectionView( + focusTitle: ritual.title, + focusTheme: ritual.theme, + dayLabel: store.ritualDayLabel(for: ritual), + completionSummary: store.completionSummary(for: ritual), + progress: store.ritualProgress(for: ritual), + habitRows: habitRows(for: ritual), + iconName: ritual.iconName, + timeOfDay: ritual.timeOfDay + ) + .frame(maxHeight: .infinity, alignment: .top) } } } } - .padding(.horizontal, Design.Spacing.large) - .padding(.bottom, Design.Spacing.large) + .padding(Design.Spacing.large) .adaptiveContentWidth() } - .coordinateSpace(name: "todayScroll") - .onPreferenceChange(TodayHeaderMinYKey.self) { headerMinY = $0 } .background(LinearGradient( colors: [AppSurface.primary, AppSurface.secondary], startPoint: .topLeading, endPoint: .bottomTrailing )) + .navigationTitle(String(localized: "Today")) + .navigationBarTitleDisplayMode(.large) .sheet(isPresented: .init( get: { showRenewalSheet }, set: { if !$0 { store.dismissRenewalPrompt() } } @@ -141,14 +99,6 @@ struct TodayView: View { } } -private struct TodayHeaderMinYKey: PreferenceKey { - static var defaultValue: CGFloat = 0 - - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value = nextValue() - } -} - #Preview { TodayView(store: RitualStore.preview, categoryStore: CategoryStore.preview) }