Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
e6a2bce76d
commit
b00b2cdafa
@ -352,6 +352,9 @@
|
|||||||
},
|
},
|
||||||
"All habits complete! Great work today." : {
|
"All habits complete! Great work today." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"All-time habits completed" : {
|
||||||
|
"comment" : "Caption for the Total Check-ins insight card."
|
||||||
},
|
},
|
||||||
"Always visible" : {
|
"Always visible" : {
|
||||||
"comment" : "Combined display name with time range for rituals that can be performed at any time.",
|
"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.",
|
"comment" : "Label text for a badge indicating that their habit completion rate is below the average for the week.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Best Streak" : {
|
|
||||||
"comment" : "Title for a stat card displaying the longest streak of days in an arc.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Best Ritual" : {
|
"Best Ritual" : {
|
||||||
"comment" : "Title for an insight card showing the highest-performing ritual by completion rate."
|
"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!" : {
|
"Best Streak" : {
|
||||||
"comment" : "Explanation for the Best Ritual insight card."
|
"comment" : "Title for a stat card displaying the longest streak of days in an arc.",
|
||||||
},
|
"isCommentAutoGenerated" : true
|
||||||
"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."
|
|
||||||
},
|
},
|
||||||
"Body scan for tension" : {
|
"Body scan for tension" : {
|
||||||
"comment" : "Habit title for a mindfulness ritual that involves scanning the body for tension.",
|
"comment" : "Habit title for a mindfulness ritual that involves scanning the body for tension.",
|
||||||
@ -1383,6 +1377,9 @@
|
|||||||
"Later" : {
|
"Later" : {
|
||||||
"comment" : "A button that dismisses the renewal prompt and returns to the main screen.",
|
"comment" : "A button that dismisses the renewal prompt and returns to the main screen.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Less" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Let go of the day" : {
|
"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.",
|
"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" : {
|
"Momentum at a glance" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -1470,6 +1468,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"More" : {
|
||||||
|
"comment" : "The text for a button that expands or collapses a list.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Morning" : {
|
"Morning" : {
|
||||||
"comment" : "Name of the time of day option for a ritual that appears in the Today view in the morning.",
|
"comment" : "Name of the time of day option for a ritual that appears in the Today view in the morning.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -1585,6 +1587,9 @@
|
|||||||
"comment" : "A description shown when a ritual does not have an active arc.",
|
"comment" : "A description shown when a ritual does not have an active arc.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"No active rituals" : {
|
||||||
|
"comment" : "Caption for the Best Ritual insight card when there are no active rituals."
|
||||||
|
},
|
||||||
"No Active Rituals" : {
|
"No Active Rituals" : {
|
||||||
"comment" : "A message displayed when a user has no active rituals.",
|
"comment" : "A message displayed when a user has no active rituals.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -2150,14 +2155,6 @@
|
|||||||
"comment" : "Notes for the \"Gratitude Practice\" ritual preset.",
|
"comment" : "Notes for the \"Gratitude Practice\" ritual preset.",
|
||||||
"isCommentAutoGenerated" : true
|
"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." : {
|
"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.",
|
"comment" : "A button that starts a ritual from a goal category. The argument is the name of the goal.",
|
||||||
"isCommentAutoGenerated" : true
|
"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" : {
|
"Start building better habits" : {
|
||||||
"comment" : "Subtitle for the \"No Active Rituals\" section in the \"Today\" view when there are no active rituals.",
|
"comment" : "Subtitle for the \"No Active Rituals\" section in the \"Today\" view when there are no active rituals.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -2394,6 +2394,9 @@
|
|||||||
"comment" : "Explanation of the value for the Insight Card titled \"Habits today\".",
|
"comment" : "Explanation of the value for the Insight Card titled \"Habits today\".",
|
||||||
"isCommentAutoGenerated" : true
|
"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" : {
|
"Theme or tagline" : {
|
||||||
"comment" : "A label for an optional tagline or theme associated with a ritual.",
|
"comment" : "A label for an optional tagline or theme associated with a ritual.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -2487,12 +2490,6 @@
|
|||||||
"Total Check-ins" : {
|
"Total Check-ins" : {
|
||||||
"comment" : "Title for an insight card showing the total number of habits completed all-time."
|
"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." : {
|
"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.",
|
"comment" : "Description of a feature card in the \"WhatsNextStepView\" that explains how to use the app's insights feature.",
|
||||||
"isCommentAutoGenerated" : true
|
"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.",
|
"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
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Weekly average" : {
|
||||||
|
"comment" : "Caption for the 7-Day Avg insight card."
|
||||||
|
},
|
||||||
"Weekly completion chart" : {
|
"Weekly completion chart" : {
|
||||||
"comment" : "An accessibility label for the weekly completion chart in the insight detail sheet.",
|
"comment" : "An accessibility label for the weekly completion chart in the insight detail sheet.",
|
||||||
"isCommentAutoGenerated" : true
|
"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.",
|
"comment" : "Name of a ritual preset that users can add to their collection, focusing on preparing for a fresh week.",
|
||||||
"isCommentAutoGenerated" : true
|
"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" : {
|
"Welcome to Rituals" : {
|
||||||
"comment" : "The title of the welcome screen in the setup wizard.",
|
"comment" : "The title of the welcome screen in the setup wizard.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -2662,6 +2656,9 @@
|
|||||||
"comment" : "Tip provided when the user is at their longest streak and it is greater than zero.",
|
"comment" : "Tip provided when the user is at their longest streak and it is greater than zero.",
|
||||||
"isCommentAutoGenerated" : true
|
"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." : {
|
"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.",
|
"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
|
"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" : {
|
"Your Journey" : {
|
||||||
"comment" : "A heading for the summary of a user's ritual progress.",
|
"comment" : "A heading for the summary of a user's ritual progress.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Your journey over time" : {
|
|
||||||
"comment" : "Subtitle for the History view, describing what the view is about.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Your next chapter" : {
|
"Your next chapter" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@ -23,7 +23,6 @@ struct HistoryView: View {
|
|||||||
@State private var monthsToShow = 2
|
@State private var monthsToShow = 2
|
||||||
@State private var refreshToken = UUID()
|
@State private var refreshToken = UUID()
|
||||||
@State private var cachedProgressByDate: [Date: Double] = [:]
|
@State private var cachedProgressByDate: [Date: Double] = [:]
|
||||||
@State private var headerMinY: CGFloat = 0
|
|
||||||
|
|
||||||
private let calendar = Calendar.current
|
private let calendar = Calendar.current
|
||||||
private let baseMonthsToShow = 2
|
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
|
/// Generate months based on expanded state
|
||||||
/// - Collapsed: Last month + current month (2 months)
|
/// - Collapsed: Last month + current month (2 months)
|
||||||
/// - Expanded: Up to 12 months of history
|
/// - Expanded: Up to 12 months of history
|
||||||
@ -106,56 +82,57 @@ struct HistoryView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
LazyVStack(alignment: .leading, spacing: Design.Spacing.large, pinnedViews: [.sectionHeaders]) {
|
VStack(alignment: .leading, spacing: Design.Spacing.large) {
|
||||||
Section(
|
// Ritual filter picker
|
||||||
header: headerSection
|
ritualPicker
|
||||||
.padding(.top, Design.Spacing.large)
|
|
||||||
.padding(.bottom, Design.Spacing.small)
|
// Month calendars - 2-column grid on iPad/landscape
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
LazyVGrid(columns: monthColumns, alignment: .leading, spacing: Design.Spacing.large) {
|
||||||
.frame(height: headerHeight, alignment: .bottom)
|
ForEach(months, id: \.self) { month in
|
||||||
.scaleEffect(headerScale, anchor: .leading)
|
HistoryMonthView(
|
||||||
.opacity(headerOpacity)
|
month: month,
|
||||||
.background(
|
selectedRitual: selectedRitual,
|
||||||
GeometryReader { proxy in
|
completionRate: { date, ritual in
|
||||||
Color.clear
|
let day = calendar.startOfDay(for: date)
|
||||||
.preference(key: HistoryHeaderMinYKey.self, value: proxy.frame(in: .named("historyScroll")).minY)
|
return cachedProgressByDate[day] ?? store.completionRate(for: day, ritual: ritual)
|
||||||
|
},
|
||||||
|
onDayTapped: { date in
|
||||||
|
selectedDateItem = IdentifiableDate(date: date)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.background(AppSurface.primary.opacity(0.9))
|
.frame(maxHeight: .infinity, alignment: .top)
|
||||||
) {
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.id(refreshToken)
|
|
||||||
}
|
}
|
||||||
|
.id(refreshToken)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Design.Spacing.large)
|
.padding(Design.Spacing.large)
|
||||||
.padding(.bottom, Design.Spacing.large)
|
|
||||||
}
|
}
|
||||||
.coordinateSpace(name: "historyScroll")
|
|
||||||
.onPreferenceChange(HistoryHeaderMinYKey.self) { headerMinY = $0 }
|
|
||||||
.background(LinearGradient(
|
.background(LinearGradient(
|
||||||
colors: [AppSurface.primary, AppSurface.secondary],
|
colors: [AppSurface.primary, AppSurface.secondary],
|
||||||
startPoint: .topLeading,
|
startPoint: .topLeading,
|
||||||
endPoint: .bottomTrailing
|
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
|
.onChange(of: store.rituals) { _, newRituals in
|
||||||
if let selectedRitual {
|
if let selectedRitual {
|
||||||
self.selectedRitual = newRituals.first { $0.id == selectedRitual.id }
|
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 {
|
private var ritualPicker: some View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
HStack(spacing: Design.Spacing.small) {
|
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 {
|
#Preview {
|
||||||
HistoryView(store: RitualStore.preview)
|
HistoryView(store: RitualStore.preview)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,11 +29,6 @@ struct InsightsView: View {
|
|||||||
NavigationStack {
|
NavigationStack {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.large) {
|
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
|
// Grid with drag-and-drop support in edit mode
|
||||||
LazyVGrid(columns: columns, spacing: Design.Spacing.medium) {
|
LazyVGrid(columns: columns, spacing: Design.Spacing.medium) {
|
||||||
ForEach(orderedCards) { card in
|
ForEach(orderedCards) { card in
|
||||||
@ -56,7 +51,7 @@ struct InsightsView: View {
|
|||||||
endPoint: .bottomTrailing
|
endPoint: .bottomTrailing
|
||||||
))
|
))
|
||||||
.navigationTitle(String(localized: "Insights"))
|
.navigationTitle(String(localized: "Insights"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .primaryAction) {
|
ToolbarItem(placement: .primaryAction) {
|
||||||
Button {
|
Button {
|
||||||
|
|||||||
@ -63,7 +63,7 @@ struct RitualsView: View {
|
|||||||
endPoint: .bottomTrailing
|
endPoint: .bottomTrailing
|
||||||
))
|
))
|
||||||
.navigationTitle(String(localized: "Rituals"))
|
.navigationTitle(String(localized: "Rituals"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .primaryAction) {
|
ToolbarItem(placement: .primaryAction) {
|
||||||
Menu {
|
Menu {
|
||||||
|
|||||||
@ -167,7 +167,7 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
.background(AppSurface.primary)
|
.background(AppSurface.primary)
|
||||||
.navigationTitle(String(localized: "Settings"))
|
.navigationTitle(String(localized: "Settings"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,6 @@ struct TodayView: View {
|
|||||||
@Bindable var store: RitualStore
|
@Bindable var store: RitualStore
|
||||||
@Bindable var categoryStore: CategoryStore
|
@Bindable var categoryStore: CategoryStore
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
@State private var headerMinY: CGFloat = 0
|
|
||||||
|
|
||||||
/// Rituals to show now based on current time of day
|
/// Rituals to show now based on current time of day
|
||||||
private var todayRituals: [Ritual] {
|
private var todayRituals: [Ritual] {
|
||||||
@ -36,88 +35,47 @@ struct TodayView: View {
|
|||||||
horizontalSizeClass: horizontalSizeClass
|
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 {
|
var body: some View {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
LazyVStack(alignment: .leading, spacing: Design.Spacing.large, pinnedViews: [.sectionHeaders]) {
|
VStack(alignment: .leading, spacing: Design.Spacing.large) {
|
||||||
Section(
|
if todayRituals.isEmpty {
|
||||||
header: TodayHeaderView(dateText: store.todayDisplayString)
|
if hasRitualsButNotNow {
|
||||||
.padding(.top, Design.Spacing.large)
|
// Has active rituals but none for current time of day
|
||||||
.padding(.bottom, Design.Spacing.small)
|
TodayNoRitualsForTimeView(store: store)
|
||||||
.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)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Use 2-column grid on iPad/landscape when multiple rituals
|
// No active rituals at all
|
||||||
LazyVGrid(columns: ritualColumns, alignment: .leading, spacing: Design.Spacing.large) {
|
TodayEmptyStateView(store: store, categoryStore: categoryStore)
|
||||||
ForEach(todayRituals) { ritual in
|
}
|
||||||
TodayRitualSectionView(
|
} else {
|
||||||
focusTitle: ritual.title,
|
// Use 2-column grid on iPad/landscape when multiple rituals
|
||||||
focusTheme: ritual.theme,
|
LazyVGrid(columns: ritualColumns, alignment: .leading, spacing: Design.Spacing.large) {
|
||||||
dayLabel: store.ritualDayLabel(for: ritual),
|
ForEach(todayRituals) { ritual in
|
||||||
completionSummary: store.completionSummary(for: ritual),
|
TodayRitualSectionView(
|
||||||
progress: store.ritualProgress(for: ritual),
|
focusTitle: ritual.title,
|
||||||
habitRows: habitRows(for: ritual),
|
focusTheme: ritual.theme,
|
||||||
iconName: ritual.iconName,
|
dayLabel: store.ritualDayLabel(for: ritual),
|
||||||
timeOfDay: ritual.timeOfDay
|
completionSummary: store.completionSummary(for: ritual),
|
||||||
)
|
progress: store.ritualProgress(for: ritual),
|
||||||
.frame(maxHeight: .infinity, alignment: .top)
|
habitRows: habitRows(for: ritual),
|
||||||
}
|
iconName: ritual.iconName,
|
||||||
|
timeOfDay: ritual.timeOfDay
|
||||||
|
)
|
||||||
|
.frame(maxHeight: .infinity, alignment: .top)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Design.Spacing.large)
|
.padding(Design.Spacing.large)
|
||||||
.padding(.bottom, Design.Spacing.large)
|
|
||||||
.adaptiveContentWidth()
|
.adaptiveContentWidth()
|
||||||
}
|
}
|
||||||
.coordinateSpace(name: "todayScroll")
|
|
||||||
.onPreferenceChange(TodayHeaderMinYKey.self) { headerMinY = $0 }
|
|
||||||
.background(LinearGradient(
|
.background(LinearGradient(
|
||||||
colors: [AppSurface.primary, AppSurface.secondary],
|
colors: [AppSurface.primary, AppSurface.secondary],
|
||||||
startPoint: .topLeading,
|
startPoint: .topLeading,
|
||||||
endPoint: .bottomTrailing
|
endPoint: .bottomTrailing
|
||||||
))
|
))
|
||||||
|
.navigationTitle(String(localized: "Today"))
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
.sheet(isPresented: .init(
|
.sheet(isPresented: .init(
|
||||||
get: { showRenewalSheet },
|
get: { showRenewalSheet },
|
||||||
set: { if !$0 { store.dismissRenewalPrompt() } }
|
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 {
|
#Preview {
|
||||||
TodayView(store: RitualStore.preview, categoryStore: CategoryStore.preview)
|
TodayView(store: RitualStore.preview, categoryStore: CategoryStore.preview)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user