diff --git a/ios/ChronoMind/App/ContentView.swift b/ios/ChronoMind/App/ContentView.swift index d394a51..cd212f6 100644 --- a/ios/ChronoMind/App/ContentView.swift +++ b/ios/ChronoMind/App/ContentView.swift @@ -1,17 +1,21 @@ // ── Main Content View with Tab Navigation ───────────────────── +// iPhone: TabView, iPad: NavigationSplitView with sidebar import SwiftUI struct ContentView: View { @EnvironmentObject var timerStore: TimerStore + @Environment(\.horizontalSizeClass) private var sizeClass @State private var selectedTab: Tab = .timeline - enum Tab: String, CaseIterable { + enum Tab: String, CaseIterable, Identifiable { case timeline = "Timeline" case focus = "Focus" case history = "History" case settings = "Settings" + var id: String { rawValue } + var icon: String { switch self { case .timeline: return "clock.fill" @@ -23,6 +27,19 @@ struct ContentView: View { } var body: some View { + Group { + if sizeClass == .regular { + iPadLayout + } else { + iPhoneLayout + } + } + .tint(CMColors.accent) + } + + // MARK: - iPhone Layout (TabView) + + private var iPhoneLayout: some View { TabView(selection: $selectedTab) { TimelineView() .tabItem { @@ -48,6 +65,249 @@ struct ContentView: View { } .tag(Tab.settings) } - .tint(CMColors.accent) + } + + // MARK: - iPad Layout (NavigationSplitView) + + private var iPadLayout: some View { + NavigationSplitView { + // Sidebar + List(Tab.allCases, selection: $selectedTab) { tab in + Label(tab.rawValue, systemImage: tab.icon) + .tag(tab) + } + .navigationTitle("ChronoMind") + .listStyle(.sidebar) + .scrollContentBackground(.hidden) + .background(CMColors.bg) + } detail: { + // Detail pane + switch selectedTab { + case .timeline: + iPadTimelineDetail + case .focus: + PomodoroView() + case .history: + HistoryView() + case .settings: + SettingsView() + } + } + .navigationSplitViewStyle(.balanced) + } + + // MARK: - iPad Timeline with Side Detail + + @State private var selectedTimerId: String? + + private var iPadTimelineDetail: some View { + HStack(spacing: 0) { + // Left: Timeline + TimelineView() + .frame(maxWidth: .infinity) + + // Right: Selected timer detail (if any) + if let timerId = selectedTimerId, + let timer = timerStore.getTimer(timerId) { + Divider() + .background(CMColors.border) + + iPadTimerDetail(timer: timer) + .frame(width: 340) + .transition(.move(edge: .trailing)) + } + } + .animation(.easeInOut(duration: 0.25), value: selectedTimerId) + } + + private func iPadTimerDetail(timer: CMTimer) -> some View { + ScrollView { + VStack(alignment: .leading, spacing: CMSpacing.lg) { + // Header + HStack { + Text("Timer Detail") + .font(CMFonts.body(size: 11, weight: .bold)) + .foregroundStyle(CMColors.textMuted) + .tracking(1.5) + Spacer() + Button { + selectedTimerId = nil + } label: { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(CMColors.textMuted) + } + } + + // Timer info + VStack(alignment: .leading, spacing: CMSpacing.sm) { + HStack { + Text(timer.label) + .font(CMFonts.display(size: 22)) + .foregroundStyle(CMColors.text) + Spacer() + UrgencyBadge(urgency: timer.urgency) + } + + Text(timer.type.label) + .font(CMFonts.body(size: 13)) + .foregroundStyle(CMColors.textSecondary) + } + + // Countdown + CountdownRing( + timer: timer, + now: timerStore.now, + size: 180 + ) + .frame(maxWidth: .infinity) + + // Cascade progress + if !timer.warnings.isEmpty { + VStack(alignment: .leading, spacing: CMSpacing.sm) { + Text("CASCADE WARNINGS") + .font(CMFonts.body(size: 11, weight: .bold)) + .foregroundStyle(CMColors.textMuted) + .tracking(1.5) + + CascadeProgressBar( + fired: timer.warnings.filter(\.fired).count, + total: timer.warnings.count, + urgency: timer.urgency + ) + + ForEach(timer.warnings) { warning in + HStack { + Image(systemName: warning.fired ? "checkmark.circle.fill" : "circle") + .font(.caption) + .foregroundStyle(warning.fired ? CMColors.success : CMColors.textMuted) + Text("\(warning.minutesBefore)m before") + .font(CMFonts.body(size: 13)) + .foregroundStyle(warning.fired ? CMColors.text : CMColors.textSecondary) + Spacer() + Text(formatTime(warning.scheduledTime)) + .font(CMFonts.mono(size: 12)) + .foregroundStyle(CMColors.textMuted) + } + } + } + } + + // Pomodoro info + if let pomState = timer.pomodoroState, let pomConfig = timer.pomodoroConfig { + VStack(alignment: .leading, spacing: CMSpacing.sm) { + Text("POMODORO") + .font(CMFonts.body(size: 11, weight: .bold)) + .foregroundStyle(CMColors.textMuted) + .tracking(1.5) + + HStack { + Text("Round \(pomState.currentRound)/\(pomConfig.rounds)") + .font(CMFonts.body(size: 14, weight: .medium)) + .foregroundStyle(CMColors.text) + Spacer() + Text(pomState.isBreak ? "Break" : "Work") + .font(CMFonts.body(size: 13, weight: .semibold)) + .foregroundStyle(pomState.isBreak ? CMColors.success : CMColors.accent) + .padding(.horizontal, CMSpacing.md) + .padding(.vertical, CMSpacing.xxs) + .background((pomState.isBreak ? CMColors.success : CMColors.accent).opacity(0.15)) + .clipShape(Capsule()) + } + } + } + + // Actions + HStack(spacing: CMSpacing.md) { + if timer.state == .active || timer.state == .warning { + Button { + HapticEngine.tap() + timerStore.pause(timer.id) + } label: { + Label("Pause", systemImage: "pause.fill") + .font(CMFonts.body(size: 14, weight: .semibold)) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, CMSpacing.sm) + .background(CMColors.important) + .clipShape(RoundedRectangle(cornerRadius: CMRadius.sm)) + } + } + + if timer.state == .paused { + Button { + HapticEngine.tap() + timerStore.resume(timer.id) + } label: { + Label("Resume", systemImage: "play.fill") + .font(CMFonts.body(size: 14, weight: .semibold)) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, CMSpacing.sm) + .background(CMColors.accent) + .clipShape(RoundedRectangle(cornerRadius: CMRadius.sm)) + } + } + + if timer.state == .firing { + Button { + HapticEngine.tap() + timerStore.dismiss(timer.id) + selectedTimerId = nil + } label: { + Label("Dismiss", systemImage: "xmark") + .font(CMFonts.body(size: 14, weight: .semibold)) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, CMSpacing.sm) + .background(CMColors.error) + .clipShape(RoundedRectangle(cornerRadius: CMRadius.sm)) + } + } + + Button { + HapticEngine.tap() + timerStore.removeTimer(timer.id) + selectedTimerId = nil + } label: { + Image(systemName: "trash") + .font(.body.weight(.semibold)) + .foregroundStyle(CMColors.error) + .padding(CMSpacing.sm) + .background(CMColors.error.opacity(0.1)) + .clipShape(RoundedRectangle(cornerRadius: CMRadius.sm)) + } + } + + // Metadata + VStack(alignment: .leading, spacing: CMSpacing.xs) { + Text("DETAILS") + .font(CMFonts.body(size: 11, weight: .bold)) + .foregroundStyle(CMColors.textMuted) + .tracking(1.5) + + metadataRow("Created", value: formatDateTime(timer.createdAt)) + metadataRow("Target", value: formatDateTime(timer.targetTime)) + metadataRow("State", value: timer.state.rawValue) + metadataRow("Snoozes", value: "\(timer.snoozeCount)") + if let category = timer.category { + metadataRow("Category", value: category) + } + } + } + .padding(CMSpacing.lg) + } + .background(CMColors.bg) + } + + private func metadataRow(_ label: String, value: String) -> some View { + HStack { + Text(label) + .font(CMFonts.body(size: 12)) + .foregroundStyle(CMColors.textMuted) + Spacer() + Text(value) + .font(CMFonts.mono(size: 12)) + .foregroundStyle(CMColors.textSecondary) + } } }