Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-26 19:17:16 -06:00
parent 576afad258
commit 1f55b1b1f2
15 changed files with 368 additions and 58 deletions

View File

@ -1,8 +1,26 @@
import Foundation import Foundation
/// Identifies each type of insight card for ordering and identification.
enum InsightCardType: String, CaseIterable, Codable {
case active
case streak
case habitsToday
case completion
case daysActive
case sevenDayAvg
case totalCheckIns
case bestRitual
/// Default order for cards (prioritizes motivation action context history)
static var defaultOrder: [InsightCardType] {
[.streak, .completion, .habitsToday, .sevenDayAvg, .active, .daysActive, .bestRitual, .totalCheckIns]
}
}
/// A single insight metric displayed on the Insights tab. /// A single insight metric displayed on the Insights tab.
struct InsightCard: Identifiable { struct InsightCard: Identifiable {
let id: UUID let id: UUID
let type: InsightCardType
let title: String let title: String
let value: String let value: String
let caption: String // Short description shown on card let caption: String // Short description shown on card
@ -13,6 +31,7 @@ struct InsightCard: Identifiable {
init( init(
id: UUID = UUID(), id: UUID = UUID(),
type: InsightCardType,
title: String, title: String,
value: String, value: String,
caption: String, caption: String,
@ -22,6 +41,7 @@ struct InsightCard: Identifiable {
trendData: [TrendDataPoint]? = nil trendData: [TrendDataPoint]? = nil
) { ) {
self.id = id self.id = id
self.type = type
self.title = title self.title = title
self.value = value self.value = value
self.caption = caption self.caption = caption

View File

@ -479,8 +479,10 @@ final class RitualStore: RitualStoreProviding {
return best return best
}() }()
return [ // Build cards dictionary by type
InsightCard( let cardsByType: [InsightCardType: InsightCard] = [
.active: InsightCard(
type: .active,
title: String(localized: "Active"), title: String(localized: "Active"),
value: "\(activeRitualCount)", value: "\(activeRitualCount)",
caption: String(localized: "In progress now"), caption: String(localized: "In progress now"),
@ -488,7 +490,8 @@ final class RitualStore: RitualStoreProviding {
symbolName: "sparkles", symbolName: "sparkles",
breakdown: currentRituals.map { BreakdownItem(label: $0.title, value: $0.theme) } breakdown: currentRituals.map { BreakdownItem(label: $0.title, value: $0.theme) }
), ),
InsightCard( .streak: InsightCard(
type: .streak,
title: String(localized: "Streak"), title: String(localized: "Streak"),
value: "\(current)", value: "\(current)",
caption: String(localized: "Consecutive perfect days"), caption: String(localized: "Consecutive perfect days"),
@ -496,7 +499,8 @@ final class RitualStore: RitualStoreProviding {
symbolName: "flame.fill", symbolName: "flame.fill",
breakdown: streakBreakdown breakdown: streakBreakdown
), ),
InsightCard( .habitsToday: InsightCard(
type: .habitsToday,
title: String(localized: "Habits today"), title: String(localized: "Habits today"),
value: "\(completedToday)", value: "\(completedToday)",
caption: String(localized: "Completed today"), caption: String(localized: "Completed today"),
@ -504,7 +508,8 @@ final class RitualStore: RitualStoreProviding {
symbolName: "checkmark.circle.fill", symbolName: "checkmark.circle.fill",
breakdown: habitsBreakdown breakdown: habitsBreakdown
), ),
InsightCard( .completion: InsightCard(
type: .completion,
title: String(localized: "Completion"), title: String(localized: "Completion"),
value: "\(completionRateValue)%", value: "\(completionRateValue)%",
caption: String(localized: "Today's progress"), caption: String(localized: "Today's progress"),
@ -513,7 +518,8 @@ final class RitualStore: RitualStoreProviding {
breakdown: trendBreakdown, breakdown: trendBreakdown,
trendData: trendData trendData: trendData
), ),
InsightCard( .daysActive: InsightCard(
type: .daysActive,
title: String(localized: "Days Active"), title: String(localized: "Days Active"),
value: "\(daysActiveCount)", value: "\(daysActiveCount)",
caption: String(localized: "Days you checked in"), caption: String(localized: "Days you checked in"),
@ -521,7 +527,8 @@ final class RitualStore: RitualStoreProviding {
symbolName: "calendar", symbolName: "calendar",
breakdown: daysActiveBreakdown() breakdown: daysActiveBreakdown()
), ),
InsightCard( .sevenDayAvg: InsightCard(
type: .sevenDayAvg,
title: String(localized: "7-Day Avg"), title: String(localized: "7-Day Avg"),
value: "\(weeklyAvg)%", value: "\(weeklyAvg)%",
caption: String(localized: "Weekly average"), caption: String(localized: "Weekly average"),
@ -530,16 +537,18 @@ final class RitualStore: RitualStoreProviding {
breakdown: trendBreakdown, breakdown: trendBreakdown,
trendData: trendData trendData: trendData
), ),
InsightCard( .totalCheckIns: InsightCard(
type: .totalCheckIns,
title: String(localized: "Total Check-ins"), title: String(localized: "Total Check-ins"),
value: "\(totalHabitsAllTime)", value: "\(totalHabitsAllTime)",
caption: String(localized: "All-time habits completed"), caption: String(localized: "All-time habits completed"),
explanation: String(localized: "The total number of habit check-ins you've made since you started using Rituals. Every check-in counts toward building lasting change."), explanation: String(localized: "The total number of habit check-ins you've made since you started using Rituals. Every check-in counts toward building lasting change."),
symbolName: "checkmark.seal.fill" symbolName: "checkmark.seal.fill"
), ),
{ .bestRitual: {
if let best = bestRitualInfo { if let best = bestRitualInfo {
return InsightCard( return InsightCard(
type: .bestRitual,
title: String(localized: "Best Ritual"), title: String(localized: "Best Ritual"),
value: "\(best.rate)%", value: "\(best.rate)%",
caption: best.title, caption: best.title,
@ -548,6 +557,7 @@ final class RitualStore: RitualStoreProviding {
) )
} else { } else {
return InsightCard( return InsightCard(
type: .bestRitual,
title: String(localized: "Best Ritual"), title: String(localized: "Best Ritual"),
value: "", value: "",
caption: String(localized: "No active rituals"), caption: String(localized: "No active rituals"),
@ -557,6 +567,43 @@ final class RitualStore: RitualStoreProviding {
} }
}() }()
] ]
// Return cards in user's preferred order
let order = insightCardOrder
return order.compactMap { cardsByType[$0] }
}
// MARK: - Insight Card Order
private static let insightCardOrderKey = "insightCardOrder"
/// The user's preferred order for insight cards
var insightCardOrder: [InsightCardType] {
get {
guard let data = UserDefaults.standard.data(forKey: Self.insightCardOrderKey),
let order = try? JSONDecoder().decode([InsightCardType].self, from: data) else {
return InsightCardType.defaultOrder
}
// Ensure all card types are included (in case new ones were added)
let missingTypes = InsightCardType.allCases.filter { !order.contains($0) }
return order + missingTypes
}
set {
if let data = try? JSONEncoder().encode(newValue) {
UserDefaults.standard.set(data, forKey: Self.insightCardOrderKey)
}
insightCardsNeedRefresh = true
}
}
/// Reorders insight cards by moving a card from one position to another
func reorderInsightCards(from sourceIndex: Int, to destinationIndex: Int) {
var order = insightCardOrder
guard sourceIndex >= 0 && sourceIndex < order.count else { return }
guard destinationIndex >= 0 && destinationIndex < order.count else { return }
let item = order.remove(at: sourceIndex)
order.insert(item, at: destinationIndex)
insightCardOrder = order
} }
func createQuickRitual() { func createQuickRitual() {

View File

@ -76,7 +76,7 @@ struct HistoryDayDetailSheet: View {
} }
} }
.presentationBackground(AppSurface.primary) .presentationBackground(AppSurface.primary)
.presentationDetents([.medium, .large]) .presentationDetents([.large])
.presentationDragIndicator(.visible) .presentationDragIndicator(.visible)
} }

View File

@ -90,7 +90,7 @@ struct HistoryView: View {
ritualPicker ritualPicker
// Month calendars - 2-column grid on iPad/landscape // Month calendars - 2-column grid on iPad/landscape
LazyVGrid(columns: monthColumns, alignment: .top, spacing: Design.Spacing.large) { LazyVGrid(columns: monthColumns, alignment: .leading, spacing: Design.Spacing.large) {
ForEach(months, id: \.self) { month in ForEach(months, id: \.self) { month in
HistoryMonthView( HistoryMonthView(
month: month, month: month,
@ -103,6 +103,7 @@ struct HistoryView: View {
selectedDateItem = IdentifiableDate(date: date) selectedDateItem = IdentifiableDate(date: date)
} }
) )
.frame(maxHeight: .infinity, alignment: .top)
} }
} }
.id(refreshToken) .id(refreshToken)

View File

@ -86,6 +86,7 @@ struct InsightCardView: View {
#Preview { #Preview {
InsightCardView( InsightCardView(
card: InsightCard( card: InsightCard(
type: .completion,
title: "Completion", title: "Completion",
value: "72%", value: "72%",
caption: "Today's progress", caption: "Today's progress",

View File

@ -74,6 +74,7 @@ struct InsightDetailSheet: View {
} }
} }
} }
.presentationDetents([.large])
} }
// MARK: - Header Section // MARK: - Header Section
@ -272,6 +273,7 @@ struct InsightDetailSheet: View {
#Preview { #Preview {
InsightDetailSheet( InsightDetailSheet(
card: InsightCard( card: InsightCard(
type: .daysActive,
title: "Ritual days", title: "Ritual days",
value: "16", value: "16",
caption: "Days on your journey", caption: "Days on your journey",

View File

@ -1,9 +1,14 @@
import SwiftUI import SwiftUI
import Bedrock import Bedrock
import UniformTypeIdentifiers
struct InsightsView: View { struct InsightsView: View {
@Bindable var store: RitualStore @Bindable var store: RitualStore
@State private var refreshToken = UUID() @State private var refreshToken = UUID()
@State private var isEditing = false
@State private var draggingCard: InsightCardType?
@State private var cardOrder: [InsightCardType] = []
@State private var lastDroppedCard: InsightCardType?
private let columns = [ private let columns = [
GridItem( GridItem(
@ -13,7 +18,15 @@ struct InsightsView: View {
) )
] ]
/// Cards in the current display order (uses local state during editing)
private var orderedCards: [InsightCard] {
let allCards = store.insightCards()
let cardsByType = Dictionary(uniqueKeysWithValues: allCards.map { ($0.type, $0) })
return cardOrder.compactMap { cardsByType[$0] }
}
var body: some View { var body: some View {
NavigationStack {
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: Design.Spacing.large) { VStack(alignment: .leading, spacing: Design.Spacing.large) {
SectionHeaderView( SectionHeaderView(
@ -21,12 +34,19 @@ struct InsightsView: View {
subtitle: String(localized: "Momentum at a glance") subtitle: String(localized: "Momentum at a glance")
) )
// Grid with drag-and-drop support in edit mode
LazyVGrid(columns: columns, spacing: Design.Spacing.medium) { LazyVGrid(columns: columns, spacing: Design.Spacing.medium) {
ForEach(store.insightCards()) { card in ForEach(orderedCards) { card in
InsightCardView(card: card, store: store) insightCardItem(card: card)
} }
} }
.id(refreshToken) .animation(.spring(response: 0.3, dampingFraction: 0.7), value: cardOrder)
.onDrop(of: [.text], isTargeted: nil) { _ in
// Fallback drop handler - clear dragging state
lastDroppedCard = draggingCard
draggingCard = nil
return false
}
} }
.padding(Design.Spacing.large) .padding(Design.Spacing.large)
} }
@ -35,7 +55,35 @@ struct InsightsView: View {
startPoint: .topLeading, startPoint: .topLeading,
endPoint: .bottomTrailing endPoint: .bottomTrailing
)) ))
.navigationTitle(String(localized: "Insights"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
withAnimation(.easeInOut(duration: Design.Animation.standard)) {
if isEditing {
// Save order and clear all drag state when exiting edit mode
draggingCard = nil
lastDroppedCard = nil
store.insightCardOrder = cardOrder
}
isEditing.toggle()
}
} label: {
Text(isEditing ? String(localized: "Done") : String(localized: "Edit"))
.foregroundStyle(AppAccent.primary)
}
}
}
.onChange(of: isEditing) { _, newValue in
// Clear all drag state when exiting edit mode
if !newValue {
draggingCard = nil
lastDroppedCard = nil
}
}
.onAppear { .onAppear {
cardOrder = store.insightCardOrder
Task { Task {
await Task.yield() await Task.yield()
store.refreshAnalyticsIfNeeded() store.refreshAnalyticsIfNeeded()
@ -45,7 +93,125 @@ struct InsightsView: View {
.onChange(of: store.rituals) { _, _ in .onChange(of: store.rituals) { _, _ in
store.refreshAnalyticsIfNeeded() store.refreshAnalyticsIfNeeded()
store.refreshInsightCardsIfNeeded() store.refreshInsightCardsIfNeeded()
refreshToken = UUID() cardOrder = store.insightCardOrder
}
.onChange(of: store.insightCardOrder) { _, newOrder in
if !isEditing {
cardOrder = newOrder
}
}
}
}
// MARK: - Card Item with Drag Support
@ViewBuilder
private func insightCardItem(card: InsightCard) -> some View {
let isDragging = draggingCard == card.type
let cardView = InsightCardView(card: card, store: store)
.overlay(alignment: .topTrailing) {
// Show drag handle in edit mode - sized to cover the disclosure chevron
if isEditing {
Image(systemName: "line.3.horizontal")
.font(.body.weight(.medium))
.foregroundStyle(AppTextColors.inverse)
.padding(Design.Spacing.small)
.background(AppAccent.primary)
.clipShape(.rect(cornerRadius: Design.CornerRadius.small))
.padding(Design.Spacing.xSmall)
}
}
.opacity(isDragging ? 0.3 : 1.0)
.scaleEffect(isDragging ? 1.05 : (isEditing ? 0.98 : 1.0))
.modifier(JiggleModifier(isEnabled: isEditing && draggingCard == nil))
// Only enable drag when in edit mode
if isEditing {
cardView
.onDrag {
// Prevent spurious re-trigger: if this is the same card that was just dropped
// and we're not currently dragging, ignore the call
let isSameAsLastDrop = lastDroppedCard == card.type
if !isSameAsLastDrop {
lastDroppedCard = nil
self.draggingCard = card.type
} else if draggingCard == nil {
// Spurious re-trigger - don't set draggingCard
}
return NSItemProvider(object: card.type.rawValue as NSString)
}
.onDrop(of: [.text], delegate: InsightCardDropDelegate(
card: card,
cardOrder: $cardOrder,
draggingCard: $draggingCard,
lastDroppedCard: $lastDroppedCard
))
} else {
cardView
}
}
}
// MARK: - Drop Delegate
struct InsightCardDropDelegate: DropDelegate {
let card: InsightCard
@Binding var cardOrder: [InsightCardType]
@Binding var draggingCard: InsightCardType?
@Binding var lastDroppedCard: InsightCardType?
func performDrop(info: DropInfo) -> Bool {
// Record which card was dropped BEFORE clearing draggingCard
// This prevents spurious onDrag re-triggers from affecting state
lastDroppedCard = draggingCard
draggingCard = nil
return true
}
func dropEntered(info: DropInfo) {
guard let dragging = draggingCard, dragging != card.type else { return }
guard let fromIndex = cardOrder.firstIndex(of: dragging),
let toIndex = cardOrder.firstIndex(of: card.type),
fromIndex != toIndex else {
return
}
// Move the item with animation - updates local state, persisted when Done is tapped
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
let item = cardOrder.remove(at: fromIndex)
cardOrder.insert(item, at: toIndex)
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
DropProposal(operation: .move)
}
func dropExited(info: DropInfo) {
// Don't clear here - wait for performDrop
}
}
// MARK: - Jiggle Animation Modifier
struct JiggleModifier: ViewModifier {
let isEnabled: Bool
@State private var isJiggling = false
func body(content: Content) -> some View {
content
.rotationEffect(.degrees(isEnabled && isJiggling ? -1 : 0))
.animation(
isEnabled ? Animation.easeInOut(duration: 0.1).repeatForever(autoreverses: true) : .default,
value: isJiggling
)
.onChange(of: isEnabled) { _, newValue in
isJiggling = newValue
}
.onAppear {
isJiggling = isEnabled
} }
} }
} }

View File

@ -21,10 +21,36 @@ struct ArcDetailView: View {
@Bindable var store: RitualStore @Bindable var store: RitualStore
let arc: RitualArc let arc: RitualArc
let ritual: Ritual let ritual: Ritual
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@State private var selectedDateItem: ArcIdentifiableDate? @State private var selectedDateItem: ArcIdentifiableDate?
private let calendar = Calendar.current private let calendar = Calendar.current
/// Whether to use wide layout on iPad/landscape
private var useWideLayout: Bool {
horizontalSizeClass == .regular
}
/// Grid columns for month calendars - 2 columns on regular width when multiple months
private var monthColumns: [GridItem] {
AdaptiveColumns.columns(
compactCount: 1,
regularCount: monthsInArc.count > 1 ? 2 : 1,
spacing: Design.Spacing.large,
horizontalSizeClass: horizontalSizeClass
)
}
/// Grid columns for habit breakdown - 2 columns on regular width when multiple habits
private var habitColumns: [GridItem] {
AdaptiveColumns.columns(
compactCount: 1,
regularCount: habitRates.count > 1 ? 2 : 1,
spacing: Design.Spacing.medium,
horizontalSizeClass: horizontalSizeClass
)
}
private var overallCompletionRate: Int { private var overallCompletionRate: Int {
let habits = arc.habits ?? [] let habits = arc.habits ?? []
let totalCheckIns = habits.reduce(0) { $0 + $1.completedDayIDs.count } let totalCheckIns = habits.reduce(0) { $0 + $1.completedDayIDs.count }
@ -243,9 +269,10 @@ struct ArcDetailView: View {
.font(.headline) .font(.headline)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
VStack(spacing: Design.Spacing.small) { LazyVGrid(columns: habitColumns, alignment: .leading, spacing: Design.Spacing.small) {
ForEach(habitRates, id: \.habit.id) { item in ForEach(habitRates, id: \.habit.id) { item in
habitRow(habit: item.habit, rate: item.rate) habitRow(habit: item.habit, rate: item.rate)
.frame(maxHeight: .infinity, alignment: .top)
} }
} }
} }
@ -307,7 +334,8 @@ struct ArcDetailView: View {
.font(.headline) .font(.headline)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
// Show month calendars for the arc's date range // Show month calendars for the arc's date range - 2 columns on iPad
LazyVGrid(columns: monthColumns, alignment: .leading, spacing: Design.Spacing.large) {
ForEach(monthsInArc, id: \.self) { month in ForEach(monthsInArc, id: \.self) { month in
HistoryMonthView( HistoryMonthView(
month: month, month: month,
@ -322,6 +350,8 @@ struct ArcDetailView: View {
} }
} }
) )
.frame(maxHeight: .infinity, alignment: .top)
}
} }
} }
} }

View File

@ -11,20 +11,32 @@ import Bedrock
/// A view showing habit completion performance within a ritual. /// A view showing habit completion performance within a ritual.
struct HabitPerformanceView: View { struct HabitPerformanceView: View {
let habitRates: [(habit: ArcHabit, rate: Double)] let habitRates: [(habit: ArcHabit, rate: Double)]
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
private var sortedByRate: [(habit: ArcHabit, rate: Double)] { private var sortedByRate: [(habit: ArcHabit, rate: Double)] {
habitRates.sorted { $0.rate > $1.rate } habitRates.sorted { $0.rate > $1.rate }
} }
/// Grid columns for habits - 2 columns on regular width when multiple habits
private var habitColumns: [GridItem] {
AdaptiveColumns.columns(
compactCount: 1,
regularCount: habitRates.count > 1 ? 2 : 1,
spacing: Design.Spacing.medium,
horizontalSizeClass: horizontalSizeClass
)
}
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(String(localized: "Habit Performance")) Text(String(localized: "Habit Performance"))
.font(.headline) .font(.headline)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
VStack(spacing: Design.Spacing.xSmall) { LazyVGrid(columns: habitColumns, alignment: .leading, spacing: Design.Spacing.xSmall) {
ForEach(sortedByRate, id: \.habit.id) { item in ForEach(sortedByRate, id: \.habit.id) { item in
habitRow(item.habit, rate: item.rate) habitRow(item.habit, rate: item.rate)
.frame(maxHeight: .infinity, alignment: .top)
} }
} }
} }

View File

@ -5,6 +5,7 @@ struct RitualDetailView: View {
@Bindable var store: RitualStore @Bindable var store: RitualStore
@Bindable var categoryStore: CategoryStore @Bindable var categoryStore: CategoryStore
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
private let ritual: Ritual private let ritual: Ritual
@ -19,6 +20,32 @@ struct RitualDetailView: View {
self.ritual = ritual self.ritual = ritual
} }
/// Whether to use wide layout on iPad/landscape
private var useWideLayout: Bool {
horizontalSizeClass == .regular
}
/// Grid columns for habits - 2 columns on regular width when multiple habits
private var habitColumns: [GridItem] {
let habits = store.habits(for: ritual)
return AdaptiveColumns.columns(
compactCount: 1,
regularCount: habits.count > 1 ? 2 : 1,
spacing: Design.Spacing.medium,
horizontalSizeClass: horizontalSizeClass
)
}
/// Grid columns for arc history - 2 columns on regular width when multiple arcs
private var arcColumns: [GridItem] {
AdaptiveColumns.columns(
compactCount: 1,
regularCount: completedArcs.count > 1 ? 2 : 1,
spacing: Design.Spacing.medium,
horizontalSizeClass: horizontalSizeClass
)
}
private var daysRemaining: Int { private var daysRemaining: Int {
store.daysRemaining(for: ritual) store.daysRemaining(for: ritual)
} }
@ -275,7 +302,7 @@ struct RitualDetailView: View {
subtitle: String(localized: "Tap to check in") subtitle: String(localized: "Tap to check in")
) )
VStack(spacing: Design.Spacing.medium) { LazyVGrid(columns: habitColumns, alignment: .leading, spacing: Design.Spacing.medium) {
ForEach(store.habits(for: ritual)) { habit in ForEach(store.habits(for: ritual)) { habit in
TodayHabitRowView( TodayHabitRowView(
title: habit.title, title: habit.title,
@ -283,6 +310,7 @@ struct RitualDetailView: View {
isCompleted: store.isHabitCompletedToday(habit), isCompleted: store.isHabitCompletedToday(habit),
action: { store.toggleHabitCompletion(habit) } action: { store.toggleHabitCompletion(habit) }
) )
.frame(maxHeight: .infinity, alignment: .top)
} }
} }
} }
@ -378,9 +406,10 @@ struct RitualDetailView: View {
.font(.caption) .font(.caption)
.foregroundStyle(AppTextColors.tertiary) .foregroundStyle(AppTextColors.tertiary)
} else { } else {
VStack(spacing: Design.Spacing.small) { LazyVGrid(columns: arcColumns, alignment: .leading, spacing: Design.Spacing.small) {
ForEach(completedArcs) { arc in ForEach(completedArcs) { arc in
arcHistoryRow(arc) arcHistoryRow(arc)
.frame(maxHeight: .infinity, alignment: .top)
} }
} }
} }

View File

@ -58,6 +58,7 @@ struct ArcRenewalSheet: View {
.sheet(isPresented: $showingEditSheet) { .sheet(isPresented: $showingEditSheet) {
RitualEditSheet(store: store, categoryStore: categoryStore, ritual: ritual) RitualEditSheet(store: store, categoryStore: categoryStore, ritual: ritual)
} }
.presentationDetents([.large])
} }
private var celebrationHeader: some View { private var celebrationHeader: some View {

View File

@ -194,7 +194,7 @@ struct PresetDetailSheet: View {
} }
} }
} }
.presentationDetents([.medium, .large]) .presentationDetents([.large])
.presentationDragIndicator(.visible) .presentationDragIndicator(.visible)
} }

View File

@ -554,7 +554,7 @@ struct IconPickerSheet: View {
} }
} }
} }
.presentationDetents([.medium, .large]) .presentationDetents([.large])
.presentationDragIndicator(.visible) .presentationDragIndicator(.visible)
} }
@ -670,7 +670,7 @@ struct HabitIconPickerSheet: View {
} }
} }
} }
.presentationDetents([.medium, .large]) .presentationDetents([.large])
.presentationDragIndicator(.visible) .presentationDragIndicator(.visible)
} }

View File

@ -119,7 +119,7 @@ struct CategoryEditSheet: View {
Text(String(localized: "Rituals using this category will be set to no category.")) Text(String(localized: "Rituals using this category will be set to no category."))
} }
} }
.presentationDetents([.medium]) .presentationDetents([.large])
} }
private func loadCategory() { private func loadCategory() {

View File

@ -51,7 +51,7 @@ struct TodayView: View {
} }
} else { } else {
// Use 2-column grid on iPad/landscape when multiple rituals // Use 2-column grid on iPad/landscape when multiple rituals
LazyVGrid(columns: ritualColumns, alignment: .top, spacing: Design.Spacing.large) { LazyVGrid(columns: ritualColumns, alignment: .leading, spacing: Design.Spacing.large) {
ForEach(todayRituals) { ritual in ForEach(todayRituals) { ritual in
TodayRitualSectionView( TodayRitualSectionView(
focusTitle: ritual.title, focusTitle: ritual.title,
@ -63,6 +63,7 @@ struct TodayView: View {
iconName: ritual.iconName, iconName: ritual.iconName,
timeOfDay: ritual.timeOfDay timeOfDay: ritual.timeOfDay
) )
.frame(maxHeight: .infinity, alignment: .top)
} }
} }
} }