2.0: banner export descartable + slider calendario/platos en iPad

- El banner "Your week is ready to export" ahora tiene botón de cerrar (X)
  que lo oculta para esa semana (persistido en AppStorage).
- iPad/Mac: handle arrastrable entre calendario y platos para rebalancear
  el alto (fracción persistida, clamp 0.28–0.72); el calendario y la lista
  se recalculan al arrastrar. Sustituye la altura fija anterior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3HaWmmtTQ1vdTERtSYU6p
This commit is contained in:
alexandrev-tibco
2026-07-24 16:29:22 +02:00
parent b70140191a
commit 543254337f
7 changed files with 81 additions and 10 deletions
+75 -10
View File
@@ -44,14 +44,63 @@ struct HomeView: View {
private var isRunningOnMac: Bool { ProcessInfo.processInfo.isiOSAppOnMac }
private var contentHorizontalPadding: CGFloat { (isRunningOnMac || horizontalSizeClass == .regular) ? 14 : 0 }
/// On iPad/Mac (regular width) let the calendar claim roughly half of the
/// available height so it feels like the centerpiece instead of a strip at
/// the top. On iPhone (compact) return nil to keep the intrinsic sizing.
/// User-adjustable split (iPad/Mac) between the calendar and the dish
/// drawer, dragged via the resize handle. 0.5 = half each.
@AppStorage("calendarHeightFraction") private var calendarFraction: Double = 0.5
@State private var dragStartFraction: Double?
/// Weeks whose export banner the user has dismissed (comma-joined week keys).
@AppStorage("dismissedExportWeeks") private var dismissedExportWeeksRaw: String = ""
private func exportWeekKey(_ plan: WeekPlan) -> String {
String(Int(plan.weekStartDate.timeIntervalSince1970))
}
private func isExportCalloutDismissed(_ plan: WeekPlan) -> Bool {
dismissedExportWeeksRaw.split(separator: ",").contains(Substring(exportWeekKey(plan)))
}
private func dismissExportCallout(_ plan: WeekPlan) {
var keys = Set(dismissedExportWeeksRaw.split(separator: ",").map(String.init))
keys.insert(exportWeekKey(plan))
// Cap the stored history so it can't grow unbounded.
dismissedExportWeeksRaw = keys.sorted().suffix(24).joined(separator: ",")
}
private static let minCalendarFraction: Double = 0.28
private static let maxCalendarFraction: Double = 0.72
/// On iPad/Mac (regular width) the calendar takes `calendarFraction` of the
/// available height (draggable), keeping a floor for the dish drawer. On
/// iPhone (compact) return nil to keep the intrinsic sizing.
private func calendarHeight(availableHeight: CGFloat) -> CGFloat? {
guard horizontalSizeClass == .regular, availableHeight > 0 else { return nil }
let target = availableHeight * 0.5
// Keep at least ~260pt for the dish drawer below.
return min(max(target, 300), availableHeight - 260)
let target = availableHeight * calendarFraction
// Keep a sensible floor for both the calendar and the dish drawer below.
return min(max(target, 200), availableHeight - 200)
}
/// Draggable handle (iPad/Mac only) to rebalance calendar vs. dishes.
private func calendarResizeHandle(availableHeight: CGFloat) -> some View {
VStack(spacing: 3) {
RoundedRectangle(cornerRadius: 2.5)
.fill(Color.mealMoodTextSecondary.opacity(0.35))
.frame(width: 40, height: 5)
}
.frame(maxWidth: .infinity)
.frame(height: 20)
.contentShape(Rectangle())
.gesture(
DragGesture()
.onChanged { value in
guard availableHeight > 0 else { return }
let base = dragStartFraction ?? calendarFraction
if dragStartFraction == nil { dragStartFraction = base }
let delta = value.translation.height / availableHeight
calendarFraction = min(Self.maxCalendarFraction,
max(Self.minCalendarFraction, base + delta))
}
.onEnded { _ in dragStartFraction = nil }
)
.accessibilityLabel(Text("calendar_resize_hint"))
}
var body: some View {
@@ -276,8 +325,12 @@ struct HomeView: View {
)
.frame(height: calendarHeight(availableHeight: contentGeo.size.height))
if horizontalSizeClass == .regular {
calendarResizeHandle(availableHeight: contentGeo.size.height)
}
let filledCount = plan.slotList.filter { $0.dishId != nil || $0.isEatingOut }.count
if filledCount > 0 {
if filledCount > 0 && !isExportCalloutDismissed(plan) {
exportCallout(plan: plan, settings: settings)
}
@@ -872,9 +925,21 @@ struct HomeView: View {
@ViewBuilder
private func exportCallout(plan: WeekPlan, settings: AppSettings) -> some View {
VStack(alignment: .leading, spacing: 10) {
Text("share_week_callout_title")
.font(.mealMoodBodyBold)
.foregroundColor(.mealMoodTextPrimary)
HStack(alignment: .top) {
Text("share_week_callout_title")
.font(.mealMoodBodyBold)
.foregroundColor(.mealMoodTextPrimary)
Spacer()
Button {
withAnimation { dismissExportCallout(plan) }
} label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 18))
.foregroundColor(.mealMoodTextSecondary.opacity(0.7))
}
.buttonStyle(.plain)
.accessibilityLabel(Text("dish_cancel"))
}
Text("share_week_callout_subtitle")
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)