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)
|
.presentationBackground(AppSurface.primary)
|
||||||
.presentationDetents([.medium, .large])
|
.presentationDetents([.medium, .large])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
|
.presentationSizing(.form)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var emptyState: some View {
|
private var emptyState: some View {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ struct IdentifiableDate: Identifiable {
|
|||||||
|
|
||||||
struct HistoryView: View {
|
struct HistoryView: View {
|
||||||
@Bindable var store: RitualStore
|
@Bindable var store: RitualStore
|
||||||
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
@State private var selectedRitual: Ritual?
|
@State private var selectedRitual: Ritual?
|
||||||
@State private var selectedDateItem: IdentifiableDate?
|
@State private var selectedDateItem: IdentifiableDate?
|
||||||
@State private var monthsToShow = 2
|
@State private var monthsToShow = 2
|
||||||
@ -27,6 +28,21 @@ struct HistoryView: View {
|
|||||||
private let baseMonthsToShow = 2
|
private let baseMonthsToShow = 2
|
||||||
private let monthChunkSize = 6
|
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
|
/// 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
|
||||||
@ -73,7 +89,8 @@ struct HistoryView: View {
|
|||||||
// Ritual filter picker
|
// Ritual filter picker
|
||||||
ritualPicker
|
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
|
ForEach(months, id: \.self) { month in
|
||||||
HistoryMonthView(
|
HistoryMonthView(
|
||||||
month: month,
|
month: month,
|
||||||
@ -87,6 +104,7 @@ struct HistoryView: View {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.id(refreshToken)
|
.id(refreshToken)
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.large)
|
.padding(Design.Spacing.large)
|
||||||
|
|||||||
@ -74,6 +74,7 @@ struct InsightDetailSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.presentationSizing(.form)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Header Section
|
// MARK: - Header Section
|
||||||
|
|||||||
@ -85,7 +85,7 @@ struct SetupWizardView: View {
|
|||||||
.padding(.horizontal, Design.Spacing.large)
|
.padding(.horizontal, Design.Spacing.large)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content
|
// Content - constrained width on iPad for better readability
|
||||||
Group {
|
Group {
|
||||||
switch currentStep {
|
switch currentStep {
|
||||||
case .welcome:
|
case .welcome:
|
||||||
@ -128,6 +128,7 @@ struct SetupWizardView: View {
|
|||||||
WhatsNextStepView(onComplete: onComplete)
|
WhatsNextStepView(onComplete: onComplete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.adaptiveContentWidth(maxWidth: Design.Size.maxContentWidthPortrait)
|
||||||
.transition(.asymmetric(
|
.transition(.asymmetric(
|
||||||
insertion: .move(edge: .trailing).combined(with: .opacity),
|
insertion: .move(edge: .trailing).combined(with: .opacity),
|
||||||
removal: .move(edge: .leading).combined(with: .opacity)
|
removal: .move(edge: .leading).combined(with: .opacity)
|
||||||
|
|||||||
@ -94,6 +94,7 @@ struct RitualDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.large)
|
.padding(Design.Spacing.large)
|
||||||
|
.adaptiveContentWidth()
|
||||||
}
|
}
|
||||||
.background(LinearGradient(
|
.background(LinearGradient(
|
||||||
colors: [AppSurface.primary, AppSurface.secondary],
|
colors: [AppSurface.primary, AppSurface.secondary],
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import Bedrock
|
|||||||
struct RitualsView: View {
|
struct RitualsView: View {
|
||||||
@Bindable var store: RitualStore
|
@Bindable var store: RitualStore
|
||||||
@Bindable var categoryStore: CategoryStore
|
@Bindable var categoryStore: CategoryStore
|
||||||
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
@State private var selectedTab: RitualsTab = .current
|
@State private var selectedTab: RitualsTab = .current
|
||||||
@State private var showingPresetLibrary = false
|
@State private var showingPresetLibrary = false
|
||||||
@State private var showingCreateRitual = false
|
@State private var showingCreateRitual = false
|
||||||
@ -11,6 +12,16 @@ struct RitualsView: View {
|
|||||||
@State private var ritualToRestart: Ritual?
|
@State private var ritualToRestart: Ritual?
|
||||||
@State private var refreshToken = UUID()
|
@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 {
|
enum RitualsTab: String, CaseIterable {
|
||||||
case current
|
case current
|
||||||
case past
|
case past
|
||||||
@ -44,6 +55,7 @@ struct RitualsView: View {
|
|||||||
}
|
}
|
||||||
.id(refreshToken)
|
.id(refreshToken)
|
||||||
.padding(Design.Spacing.large)
|
.padding(Design.Spacing.large)
|
||||||
|
.adaptiveContentWidth()
|
||||||
}
|
}
|
||||||
.background(LinearGradient(
|
.background(LinearGradient(
|
||||||
colors: [AppSurface.primary, AppSurface.secondary],
|
colors: [AppSurface.primary, AppSurface.secondary],
|
||||||
@ -137,6 +149,8 @@ struct RitualsView: View {
|
|||||||
}
|
}
|
||||||
.padding(.top, Design.Spacing.small)
|
.padding(.top, Design.Spacing.small)
|
||||||
|
|
||||||
|
// Use 2-column grid on iPad/landscape
|
||||||
|
LazyVGrid(columns: ritualColumns, spacing: Design.Spacing.medium) {
|
||||||
ForEach(group.rituals) { ritual in
|
ForEach(group.rituals) { ritual in
|
||||||
currentRitualRow(for: ritual)
|
currentRitualRow(for: ritual)
|
||||||
}
|
}
|
||||||
@ -144,6 +158,7 @@ struct RitualsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Past Tab Content
|
// MARK: - Past Tab Content
|
||||||
|
|
||||||
@ -152,7 +167,8 @@ struct RitualsView: View {
|
|||||||
if store.pastRituals.isEmpty {
|
if store.pastRituals.isEmpty {
|
||||||
pastEmptyState
|
pastEmptyState
|
||||||
} else {
|
} 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
|
ForEach(store.pastRituals) { ritual in
|
||||||
pastRitualRow(for: ritual)
|
pastRitualRow(for: ritual)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,6 +96,7 @@ struct ArcDetailSheet: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.presentationSizing(.form)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns habit completions for a specific date within this arc
|
/// Returns habit completions for a specific date within this arc
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
.presentationSizing(.form)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var celebrationHeader: some View {
|
private var celebrationHeader: some View {
|
||||||
|
|||||||
@ -47,6 +47,7 @@ struct PresetLibrarySheet: View {
|
|||||||
}
|
}
|
||||||
.presentationDetents([.large])
|
.presentationDetents([.large])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
|
.presentationSizing(.form)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Category Picker
|
// MARK: - Category Picker
|
||||||
@ -196,6 +197,7 @@ struct PresetDetailSheet: View {
|
|||||||
}
|
}
|
||||||
.presentationDetents([.medium, .large])
|
.presentationDetents([.medium, .large])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
|
.presentationSizing(.form)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var headerSection: some View {
|
private var headerSection: some View {
|
||||||
|
|||||||
@ -93,6 +93,7 @@ struct RitualEditSheet: View {
|
|||||||
}
|
}
|
||||||
.presentationDetents([.large])
|
.presentationDetents([.large])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
|
.presentationSizing(.form)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Form Sections
|
// MARK: - Form Sections
|
||||||
@ -556,6 +557,7 @@ struct IconPickerSheet: View {
|
|||||||
}
|
}
|
||||||
.presentationDetents([.medium, .large])
|
.presentationDetents([.medium, .large])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
|
.presentationSizing(.form)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func iconButton(_ icon: String) -> some View {
|
private func iconButton(_ icon: String) -> some View {
|
||||||
@ -672,6 +674,7 @@ struct HabitIconPickerSheet: View {
|
|||||||
}
|
}
|
||||||
.presentationDetents([.medium, .large])
|
.presentationDetents([.medium, .large])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
|
.presentationSizing(.form)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func iconButton(_ icon: String) -> some View {
|
private func iconButton(_ icon: String) -> some View {
|
||||||
|
|||||||
@ -120,6 +120,7 @@ struct CategoryEditSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.presentationDetents([.medium])
|
.presentationDetents([.medium])
|
||||||
|
.presentationSizing(.form)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadCategory() {
|
private func loadCategory() {
|
||||||
|
|||||||
@ -156,6 +156,7 @@ struct SettingsView: View {
|
|||||||
Spacer(minLength: Design.Spacing.xxxLarge)
|
Spacer(minLength: Design.Spacing.xxxLarge)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Design.Spacing.large)
|
.padding(.horizontal, Design.Spacing.large)
|
||||||
|
.adaptiveContentWidth(maxWidth: Design.Size.maxContentWidthPortrait)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
store.refresh()
|
store.refresh()
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import Bedrock
|
|||||||
struct TodayView: View {
|
struct TodayView: View {
|
||||||
@Bindable var store: RitualStore
|
@Bindable var store: RitualStore
|
||||||
@Bindable var categoryStore: CategoryStore
|
@Bindable var categoryStore: CategoryStore
|
||||||
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
|
|
||||||
/// 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] {
|
||||||
@ -20,6 +21,21 @@ struct TodayView: View {
|
|||||||
store.ritualNeedingRenewal != nil
|
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 {
|
var body: some View {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.large) {
|
VStack(alignment: .leading, spacing: Design.Spacing.large) {
|
||||||
@ -34,6 +50,8 @@ struct TodayView: View {
|
|||||||
TodayEmptyStateView(store: store, categoryStore: categoryStore)
|
TodayEmptyStateView(store: store, categoryStore: categoryStore)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Use 2-column grid on iPad/landscape when multiple rituals
|
||||||
|
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,
|
||||||
@ -48,7 +66,9 @@ struct TodayView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.padding(Design.Spacing.large)
|
.padding(Design.Spacing.large)
|
||||||
|
.adaptiveContentWidth()
|
||||||
}
|
}
|
||||||
.background(LinearGradient(
|
.background(LinearGradient(
|
||||||
colors: [AppSurface.primary, AppSurface.secondary],
|
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