iPhone Duo (fase 1): navegación adaptativa por size class y layouts regular×regular

- ContentView: un único TabView (sidebarAdaptable en iOS 18+) para todas las
  size classes; desaparece el cambio de jerarquía iPhone/iPad, así abrir o
  cerrar el Duo conserva el estado de cada pestaña. @SceneStorage de la tab.
- Fuentes y Diario: NavigationSplitView (lista + detalle a la vez en ancho
  regular, stack colapsable en compacto) con selección nativa de List.
- Dashboard: Quick Update como .inspector (panel lateral junto a los gráficos
  en regular, sheet en compacto).
- Gráficos: chartHeightScale (+35% de alto en regular) vía chartFrame(height:),
  sin ignoresSafeArea en contenido interactivo.
- Liquid Glass (glassEffect) en las etiquetas flotantes del Diario en iOS 26+.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn
This commit is contained in:
alexandrev-tibco
2026-09-16 11:44:50 +02:00
parent a8a9ad64f3
commit 680255f69d
17 changed files with 485 additions and 547 deletions
@@ -10,64 +10,45 @@ struct JournalView: View {
@State private var selectedDate: Date?
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
/// Month list + inline check-in: two columns in regular width (iPad, iPhone
/// Duo inner screen), a collapsing stack in compact width. The selected
/// month is the only navigation state, so it survives the Duo opening/closing.
@State private var columnVisibility: NavigationSplitViewVisibility = .all
var body: some View {
if horizontalSizeClass == .regular {
iPadJournalLayout
} else {
iPhoneJournalLayout
}
}
// MARK: - iPad Layout (list + inline detail)
private var iPadJournalLayout: some View {
HStack(spacing: 0) {
// Left: month list
NavigationStack {
journalListContent(isPad: true)
.navigationTitle("Journal")
.searchable(text: $searchText, prompt: "Search monthly notes")
.onAppear { viewModel.refresh() }
}
.frame(width: 320)
Divider()
// Right: monthly check-in detail
NavigationSplitView(columnVisibility: $columnVisibility) {
journalListContent
.navigationTitle("Journal")
.searchable(text: $searchText, prompt: "Search monthly notes")
.onAppear { viewModel.refresh() }
.navigationSplitViewColumnWidth(min: 300, ideal: 340, max: 420)
} detail: {
NavigationStack {
if let date = selectedDate {
MonthlyCheckInView(referenceDate: date)
.id(date)
} else {
noMonthSelectedView
}
}
}
}
// MARK: - iPhone Layout (push navigation)
private var iPhoneJournalLayout: some View {
NavigationStack {
journalListContent(isPad: false)
.navigationTitle("Journal")
.searchable(text: $searchText, prompt: "Search monthly notes")
.onAppear { viewModel.refresh() }
}
.navigationSplitViewStyle(.balanced)
}
// MARK: - Shared List Content
private func journalListContent(isPad: Bool) -> some View {
private var journalListContent: some View {
ScrollViewReader { proxy in
ZStack {
AppBackground()
List {
List(selection: $selectedDate) {
// Calm 2.0: the habit story (streaks, achievements) lives in
// the Journal this is the emotional home of the ritual.
if !viewModel.monthlyNotes.isEmpty && searchText.isEmpty {
Section {
MomentumStreaksCard(compact: isPad)
// Narrow sidebar column in regular width compact stat tiles.
MomentumStreaksCard(compact: horizontalSizeClass == .regular)
}
.listRowInsets(EdgeInsets())
.listRowBackground(Color.clear)
@@ -80,12 +61,12 @@ struct JournalView: View {
VStack(spacing: 12) {
Image(systemName: "book.closed")
.font(.system(size: 36))
.foregroundColor(.appSecondary)
.foregroundStyle(Color.appSecondary)
Text(String(localized: "journal_empty_title"))
.font(.headline)
Text(String(localized: "journal_empty_body"))
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.padding(.vertical, 16)
@@ -93,34 +74,14 @@ struct JournalView: View {
} else {
Text("No matching notes.")
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
} else {
ForEach(filteredMonthlyNotes) { entry in
if isPad {
Button {
selectedDate = entry.date
} label: {
monthlyNoteRow(entry)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.listRowBackground(
selectedDate.map { $0.isSameMonth(as: entry.date) } == true
? Color.appPrimary.opacity(0.08)
: Color.clear
)
monthlyNoteRow(entry)
.tag(entry.date)
.id(entry.date)
.onAppear { currentVisibleMonth = entry.date }
} else {
NavigationLink {
MonthlyCheckInView(referenceDate: entry.date)
} label: {
monthlyNoteRow(entry)
}
.id(entry.date)
.onAppear { currentVisibleMonth = entry.date }
}
}
}
}
@@ -156,7 +117,7 @@ struct JournalView: View {
Image(systemName: "book.closed.fill")
.font(.system(size: 50, weight: .light))
.foregroundColor(.white)
.foregroundStyle(.white)
}
VStack(spacing: 10) {
@@ -164,14 +125,15 @@ struct JournalView: View {
.font(.title2.weight(.semibold))
Text("Choose a month from the list on the\nleft to view your check-in notes.")
.frame(maxWidth: 360)
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
Label("Select from the list", systemImage: "arrow.left")
.font(.subheadline.weight(.semibold))
.foregroundColor(.appSecondary)
.foregroundStyle(Color.appSecondary)
.padding(.horizontal, 22)
.padding(.vertical, 11)
.background(Color.appSecondary.opacity(0.1))
@@ -203,7 +165,7 @@ struct JournalView: View {
.frame(width: 40, height: 40)
Image(systemName: entry.mood?.iconName ?? "book.closed")
.font(.system(size: 16))
.foregroundColor(entry.mood?.tint ?? .secondary)
.foregroundStyle(entry.mood?.tint ?? .secondary)
}
VStack(alignment: .leading, spacing: 4) {
@@ -220,12 +182,12 @@ struct JournalView: View {
if let mood = entry.mood {
Text(mood.title)
.font(.caption.weight(.medium))
.foregroundColor(mood.tint)
.foregroundStyle(mood.tint)
}
Text(entry.note.isEmpty ? String(localized: "journal_no_note") : entry.note)
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.lineLimit(2)
}
}
@@ -237,7 +199,7 @@ struct JournalView: View {
return HStack(spacing: 4) {
ForEach(1...5, id: \.self) { index in
Image(systemName: index <= value ? "star.fill" : "star")
.foregroundColor(index <= value ? .yellow : .secondary)
.foregroundStyle(index <= value ? .yellow : .secondary)
}
}
.accessibilityLabel(
@@ -251,7 +213,7 @@ struct JournalView: View {
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Color.gray.opacity(0.15))
.foregroundColor(.primary)
.foregroundStyle(.primary)
.clipShape(Capsule())
}
@@ -269,9 +231,7 @@ struct JournalView: View {
.font(.caption.weight(.semibold))
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Color(.systemBackground).opacity(0.95))
.cornerRadius(12)
.shadow(color: .black.opacity(0.12), radius: 6, y: 2)
.floatingPillBackground()
.offset(x: -16)
}
@@ -280,9 +240,7 @@ struct JournalView: View {
.font(.caption.weight(.semibold))
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Color(.systemBackground).opacity(0.95))
.cornerRadius(12)
.shadow(color: .black.opacity(0.12), radius: 6, y: 2)
.floatingPillBackground()
.offset(x: -16, y: scrubberOffset - height / 2)
}
@@ -339,13 +297,13 @@ struct MonthlyNoteEditorView: View {
.frame(minHeight: 200)
.padding(8)
.background(Color.gray.opacity(0.08))
.cornerRadius(12)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.padding()
.navigationTitle("Monthly Note")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
ToolbarItem(placement: .topBarTrailing) {
Button("Save") {
MonthlyCheckInStore.setNote(note, for: date)
dismiss()