Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
dd9d9315b6
commit
9c57230e55
@ -78,6 +78,7 @@ struct HistoryDayDetailSheet: View {
|
||||
.presentationBackground(AppSurface.primary)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationSizing(.form)
|
||||
}
|
||||
|
||||
private var emptyState: some View {
|
||||
|
||||
@ -17,6 +17,7 @@ struct IdentifiableDate: Identifiable {
|
||||
|
||||
struct HistoryView: View {
|
||||
@Bindable var store: RitualStore
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@State private var selectedRitual: Ritual?
|
||||
@State private var selectedDateItem: IdentifiableDate?
|
||||
@State private var monthsToShow = 2
|
||||
@ -27,6 +28,21 @@ struct HistoryView: View {
|
||||
private let baseMonthsToShow = 2
|
||||
private let monthChunkSize = 6
|
||||
|
||||
/// Whether to use wide layout (2 columns) on iPad/landscape
|
||||
private var useWideLayout: Bool {
|
||||
horizontalSizeClass == .regular
|
||||
}
|
||||
|
||||
/// Grid columns for month cards - 2 columns on regular width, 1 on compact
|
||||
private var monthColumns: [GridItem] {
|
||||
AdaptiveColumns.columns(
|
||||
compactCount: 1,
|
||||
regularCount: 2,
|
||||
spacing: Design.Spacing.large,
|
||||
horizontalSizeClass: horizontalSizeClass
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate months based on expanded state
|
||||
/// - Collapsed: Last month + current month (2 months)
|
||||
/// - Expanded: Up to 12 months of history
|
||||
@ -73,7 +89,8 @@ struct HistoryView: View {
|
||||
// Ritual filter picker
|
||||
ritualPicker
|
||||
|
||||
// Month calendars
|
||||
// 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,
|
||||
@ -87,6 +104,7 @@ struct HistoryView: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.id(refreshToken)
|
||||
}
|
||||
.padding(Design.Spacing.large)
|
||||
|
||||
@ -74,6 +74,7 @@ struct InsightDetailSheet: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationSizing(.form)
|
||||
}
|
||||
|
||||
// MARK: - Header Section
|
||||
|
||||
@ -85,7 +85,7 @@ struct SetupWizardView: View {
|
||||
.padding(.horizontal, Design.Spacing.large)
|
||||
}
|
||||
|
||||
// Content
|
||||
// Content - constrained width on iPad for better readability
|
||||
Group {
|
||||
switch currentStep {
|
||||
case .welcome:
|
||||
@ -128,6 +128,7 @@ struct SetupWizardView: View {
|
||||
WhatsNextStepView(onComplete: onComplete)
|
||||
}
|
||||
}
|
||||
.adaptiveContentWidth(maxWidth: Design.Size.maxContentWidthPortrait)
|
||||
.transition(.asymmetric(
|
||||
insertion: .move(edge: .trailing).combined(with: .opacity),
|
||||
removal: .move(edge: .leading).combined(with: .opacity)
|
||||
|
||||
@ -94,6 +94,7 @@ struct RitualDetailView: View {
|
||||
}
|
||||
}
|
||||
.padding(Design.Spacing.large)
|
||||
.adaptiveContentWidth()
|
||||
}
|
||||
.background(LinearGradient(
|
||||
colors: [AppSurface.primary, AppSurface.secondary],
|
||||
|
||||
@ -4,6 +4,7 @@ import Bedrock
|
||||
struct RitualsView: View {
|
||||
@Bindable var store: RitualStore
|
||||
@Bindable var categoryStore: CategoryStore
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@State private var selectedTab: RitualsTab = .current
|
||||
@State private var showingPresetLibrary = false
|
||||
@State private var showingCreateRitual = false
|
||||
@ -11,6 +12,16 @@ struct RitualsView: View {
|
||||
@State private var ritualToRestart: Ritual?
|
||||
@State private var refreshToken = UUID()
|
||||
|
||||
/// Grid columns for ritual cards - 2 columns on regular width, 1 on compact
|
||||
private var ritualColumns: [GridItem] {
|
||||
AdaptiveColumns.columns(
|
||||
compactCount: 1,
|
||||
regularCount: 2,
|
||||
spacing: Design.Spacing.medium,
|
||||
horizontalSizeClass: horizontalSizeClass
|
||||
)
|
||||
}
|
||||
|
||||
enum RitualsTab: String, CaseIterable {
|
||||
case current
|
||||
case past
|
||||
@ -44,6 +55,7 @@ struct RitualsView: View {
|
||||
}
|
||||
.id(refreshToken)
|
||||
.padding(Design.Spacing.large)
|
||||
.adaptiveContentWidth()
|
||||
}
|
||||
.background(LinearGradient(
|
||||
colors: [AppSurface.primary, AppSurface.secondary],
|
||||
@ -137,6 +149,8 @@ struct RitualsView: View {
|
||||
}
|
||||
.padding(.top, Design.Spacing.small)
|
||||
|
||||
// Use 2-column grid on iPad/landscape
|
||||
LazyVGrid(columns: ritualColumns, spacing: Design.Spacing.medium) {
|
||||
ForEach(group.rituals) { ritual in
|
||||
currentRitualRow(for: ritual)
|
||||
}
|
||||
@ -144,6 +158,7 @@ struct RitualsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Past Tab Content
|
||||
|
||||
@ -152,7 +167,8 @@ struct RitualsView: View {
|
||||
if store.pastRituals.isEmpty {
|
||||
pastEmptyState
|
||||
} else {
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
// Use 2-column grid on iPad/landscape
|
||||
LazyVGrid(columns: ritualColumns, spacing: Design.Spacing.medium) {
|
||||
ForEach(store.pastRituals) { ritual in
|
||||
pastRitualRow(for: ritual)
|
||||
}
|
||||
|
||||
@ -96,6 +96,7 @@ struct ArcDetailSheet: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.presentationSizing(.form)
|
||||
}
|
||||
|
||||
/// Returns habit completions for a specific date within this arc
|
||||
|
||||
@ -58,6 +58,7 @@ struct ArcRenewalSheet: View {
|
||||
.sheet(isPresented: $showingEditSheet) {
|
||||
RitualEditSheet(store: store, categoryStore: categoryStore, ritual: ritual)
|
||||
}
|
||||
.presentationSizing(.form)
|
||||
}
|
||||
|
||||
private var celebrationHeader: some View {
|
||||
|
||||
@ -47,6 +47,7 @@ struct PresetLibrarySheet: View {
|
||||
}
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationSizing(.form)
|
||||
}
|
||||
|
||||
// MARK: - Category Picker
|
||||
@ -196,6 +197,7 @@ struct PresetDetailSheet: View {
|
||||
}
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationSizing(.form)
|
||||
}
|
||||
|
||||
private var headerSection: some View {
|
||||
|
||||
@ -93,6 +93,7 @@ struct RitualEditSheet: View {
|
||||
}
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationSizing(.form)
|
||||
}
|
||||
|
||||
// MARK: - Form Sections
|
||||
@ -556,6 +557,7 @@ struct IconPickerSheet: View {
|
||||
}
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationSizing(.form)
|
||||
}
|
||||
|
||||
private func iconButton(_ icon: String) -> some View {
|
||||
@ -672,6 +674,7 @@ struct HabitIconPickerSheet: View {
|
||||
}
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationSizing(.form)
|
||||
}
|
||||
|
||||
private func iconButton(_ icon: String) -> some View {
|
||||
|
||||
@ -120,6 +120,7 @@ struct CategoryEditSheet: View {
|
||||
}
|
||||
}
|
||||
.presentationDetents([.medium])
|
||||
.presentationSizing(.form)
|
||||
}
|
||||
|
||||
private func loadCategory() {
|
||||
|
||||
@ -156,6 +156,7 @@ struct SettingsView: View {
|
||||
Spacer(minLength: Design.Spacing.xxxLarge)
|
||||
}
|
||||
.padding(.horizontal, Design.Spacing.large)
|
||||
.adaptiveContentWidth(maxWidth: Design.Size.maxContentWidthPortrait)
|
||||
}
|
||||
.onAppear {
|
||||
store.refresh()
|
||||
|
||||
@ -4,6 +4,7 @@ import Bedrock
|
||||
struct TodayView: View {
|
||||
@Bindable var store: RitualStore
|
||||
@Bindable var categoryStore: CategoryStore
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
/// Rituals to show now based on current time of day
|
||||
private var todayRituals: [Ritual] {
|
||||
@ -20,6 +21,21 @@ struct TodayView: View {
|
||||
store.ritualNeedingRenewal != nil
|
||||
}
|
||||
|
||||
/// Whether to use wide layout on iPad/landscape
|
||||
private var useWideLayout: Bool {
|
||||
horizontalSizeClass == .regular
|
||||
}
|
||||
|
||||
/// Grid columns for ritual sections - 2 columns on regular width when multiple rituals
|
||||
private var ritualColumns: [GridItem] {
|
||||
AdaptiveColumns.columns(
|
||||
compactCount: 1,
|
||||
regularCount: todayRituals.count > 1 ? 2 : 1,
|
||||
spacing: Design.Spacing.large,
|
||||
horizontalSizeClass: horizontalSizeClass
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.large) {
|
||||
@ -34,6 +50,8 @@ struct TodayView: View {
|
||||
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,
|
||||
@ -48,7 +66,9 @@ struct TodayView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(Design.Spacing.large)
|
||||
.adaptiveContentWidth()
|
||||
}
|
||||
.background(LinearGradient(
|
||||
colors: [AppSurface.primary, AppSurface.secondary],
|
||||
|
||||
93
Andromida/Shared/AdaptiveLayout.swift
Normal file
93
Andromida/Shared/AdaptiveLayout.swift
Normal file
@ -0,0 +1,93 @@
|
||||
//
|
||||
// AdaptiveLayout.swift
|
||||
// Andromida
|
||||
//
|
||||
// Reusable layout modifiers and helpers for responsive iPad/iPhone layouts.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Bedrock
|
||||
|
||||
// MARK: - Adaptive Content Width Modifier
|
||||
|
||||
/// A view modifier that constrains content width on regular-width displays (iPad, large iPhone landscape).
|
||||
/// On compact-width displays (iPhone portrait), content remains full-width.
|
||||
struct AdaptiveContentWidth: ViewModifier {
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
let maxWidth: CGFloat
|
||||
let centerContent: Bool
|
||||
|
||||
init(maxWidth: CGFloat = Design.Size.maxContentWidthLandscape, centerContent: Bool = true) {
|
||||
self.maxWidth = maxWidth
|
||||
self.centerContent = centerContent
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if horizontalSizeClass == .regular {
|
||||
if centerContent {
|
||||
content
|
||||
.frame(maxWidth: maxWidth)
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
content
|
||||
.frame(maxWidth: maxWidth)
|
||||
}
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Constrains the view's width on regular-width displays (iPad, large iPhone landscape).
|
||||
/// - Parameters:
|
||||
/// - maxWidth: The maximum width to constrain to. Defaults to `Design.Size.maxContentWidthLandscape` (800pt).
|
||||
/// - centered: Whether to center the content horizontally. Defaults to `true`.
|
||||
/// - Returns: A view with adaptive width constraints applied.
|
||||
func adaptiveContentWidth(
|
||||
maxWidth: CGFloat = Design.Size.maxContentWidthLandscape,
|
||||
centered: Bool = true
|
||||
) -> some View {
|
||||
modifier(AdaptiveContentWidth(maxWidth: maxWidth, centerContent: centered))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Adaptive Grid Columns
|
||||
|
||||
/// Helper for creating responsive grid columns based on horizontal size class.
|
||||
enum AdaptiveColumns {
|
||||
/// Creates grid columns that adapt based on horizontal size class.
|
||||
/// - Parameters:
|
||||
/// - compactCount: Number of columns for compact width (iPhone portrait). Defaults to 1.
|
||||
/// - regularCount: Number of columns for regular width (iPad, landscape). Defaults to 2.
|
||||
/// - spacing: Spacing between columns. Defaults to `Design.Spacing.medium`.
|
||||
/// - horizontalSizeClass: The current horizontal size class.
|
||||
/// - Returns: An array of `GridItem` configured for the given size class.
|
||||
static func columns(
|
||||
compactCount: Int = 1,
|
||||
regularCount: Int = 2,
|
||||
spacing: CGFloat = Design.Spacing.medium,
|
||||
horizontalSizeClass: UserInterfaceSizeClass?
|
||||
) -> [GridItem] {
|
||||
let count = horizontalSizeClass == .regular ? regularCount : compactCount
|
||||
return Array(repeating: GridItem(.flexible(), spacing: spacing), count: count)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Use Wide Layout Helper
|
||||
|
||||
/// Environment-based helper view that provides layout decisions.
|
||||
struct AdaptiveLayoutReader<Content: View>: View {
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
let content: (_ useWideLayout: Bool) -> Content
|
||||
|
||||
init(@ViewBuilder content: @escaping (_ useWideLayout: Bool) -> Content) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
content(horizontalSizeClass == .regular)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user