// ── Menu Bar Popover ────────────────────────────────────────── // Main popover view shown when clicking the menu bar icon import SwiftUI struct MenuBarPopover: View { @EnvironmentObject var store: MacTimerStore @EnvironmentObject var menuBar: MenuBarState var body: some View { VStack(spacing: 0) { // Header header Divider().background(CMColors.border) // Quick timer creation if menuBar.showQuickTimer { quickTimerForm Divider().background(CMColors.border) } // Timer list if store.activeTimers.isEmpty && !menuBar.showQuickTimer { emptyState } else { timerList } Divider().background(CMColors.border) // Footer footer } .frame(width: 320) .background(CMColors.bg) } // MARK: - Header private var header: some View { HStack { Text("ChronoMind") .font(CMFonts.display(size: 14)) .foregroundStyle(CMColors.text) Spacer() Button { menuBar.showQuickTimer.toggle() } label: { Image(systemName: menuBar.showQuickTimer ? "xmark" : "plus") .font(.body.weight(.semibold)) .foregroundStyle(CMColors.accent) } .buttonStyle(.plain) .keyboardShortcut("t", modifiers: [.command, .shift]) } .padding(.horizontal, 12) .padding(.vertical, 10) } // MARK: - Quick Timer Form private var quickTimerForm: some View { VStack(spacing: 8) { TextField("Timer label", text: $menuBar.quickTimerLabel) .textFieldStyle(.plain) .font(CMFonts.body(size: 14)) .foregroundStyle(CMColors.text) .padding(8) .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: 6)) HStack { Text("\(Int(menuBar.quickTimerMinutes))m") .font(CMFonts.mono(size: 13, weight: .semibold)) .foregroundStyle(CMColors.accent) .frame(width: 40) Slider(value: $menuBar.quickTimerMinutes, in: 1...120, step: 1) .tint(CMColors.accent) } HStack(spacing: 8) { // Preset buttons ForEach([5, 15, 25, 60], id: \.self) { minutes in Button("\(minutes)m") { menuBar.quickTimerMinutes = Double(minutes) } .font(CMFonts.body(size: 11, weight: .medium)) .foregroundStyle(menuBar.quickTimerMinutes == Double(minutes) ? .white : CMColors.textSecondary) .padding(.horizontal, 8) .padding(.vertical, 4) .background(menuBar.quickTimerMinutes == Double(minutes) ? CMColors.accent : CMColors.surfaceHover) .clipShape(Capsule()) .buttonStyle(.plain) } Spacer() Button("Start") { let label = menuBar.quickTimerLabel.isEmpty ? "Timer" : menuBar.quickTimerLabel store.addCountdown( label: label, durationSeconds: menuBar.quickTimerMinutes * 60 ) menuBar.resetQuickTimer() } .font(CMFonts.body(size: 13, weight: .semibold)) .foregroundStyle(.white) .padding(.horizontal, 16) .padding(.vertical, 6) .background(CMColors.accent) .clipShape(Capsule()) .buttonStyle(.plain) } } .padding(12) } // MARK: - Timer List private var timerList: some View { ScrollView { LazyVStack(spacing: 1) { ForEach(store.activeTimers.sorted(by: { $0.targetTime < $1.targetTime })) { timer in MacTimerRow(timer: timer, now: store.now) } } } .frame(maxHeight: 300) } // MARK: - Empty State private var emptyState: some View { VStack(spacing: 8) { Image(systemName: "clock") .font(.system(size: 28)) .foregroundStyle(CMColors.textMuted) Text("No active timers") .font(CMFonts.body(size: 13)) .foregroundStyle(CMColors.textMuted) Text("Press ⌘⇧T to create one") .font(CMFonts.body(size: 11)) .foregroundStyle(CMColors.textMuted.opacity(0.7)) } .frame(maxWidth: .infinity) .padding(.vertical, 24) } // MARK: - Footer private var footer: some View { HStack { Text("\(store.activeTimers.count) active") .font(CMFonts.body(size: 11)) .foregroundStyle(CMColors.textMuted) Spacer() if #available(macOS 14.0, *) { SettingsLink { Text("Settings") .font(CMFonts.body(size: 11)) .foregroundStyle(CMColors.textSecondary) } .buttonStyle(.plain) } Button("Quit") { NSApplication.shared.terminate(nil) } .font(CMFonts.body(size: 11)) .foregroundStyle(CMColors.textMuted) .buttonStyle(.plain) .keyboardShortcut("q") } .padding(.horizontal, 12) .padding(.vertical, 8) } } // MARK: - Mac Timer Row struct MacTimerRow: View { let timer: CMTimer let now: Date @EnvironmentObject var store: MacTimerStore var body: some View { HStack(spacing: 8) { // Urgency dot Circle() .fill(CMColors.urgencyColor(timer.urgency)) .frame(width: 6, height: 6) // Label VStack(alignment: .leading, spacing: 2) { Text(timer.label) .font(CMFonts.body(size: 13, weight: .medium)) .foregroundStyle(CMColors.text) .lineLimit(1) Text(formatTime(timer.targetTime)) .font(CMFonts.body(size: 11)) .foregroundStyle(CMColors.textMuted) } Spacer() // Countdown let remaining = getRemainingSeconds(timer, now: now) Text(formatDuration(remaining)) .font(CMFonts.mono(size: 13, weight: .semibold)) .foregroundStyle(stateColor) // Actions if timer.state == .firing { Button { store.snooze(timer.id, minutes: 5) } label: { Image(systemName: "moon.zzz") .font(.caption) .foregroundStyle(CMColors.textMuted) } .buttonStyle(.plain) Button { store.dismiss(timer.id) } label: { Image(systemName: "xmark.circle.fill") .font(.caption) .foregroundStyle(CMColors.error) } .buttonStyle(.plain) } else { Button { store.removeTimer(timer.id) } label: { Image(systemName: "xmark") .font(.caption2.weight(.bold)) .foregroundStyle(CMColors.textMuted.opacity(0.5)) } .buttonStyle(.plain) } } .padding(.horizontal, 12) .padding(.vertical, 8) .background(timer.state == .firing ? CMColors.urgencyBg(timer.urgency) : Color.clear) .contentShape(Rectangle()) } private var stateColor: Color { switch timer.state { case .firing: return CMColors.urgencyColor(timer.urgency) case .warning: return CMColors.important case .paused: return CMColors.textMuted default: return CMColors.accent } } }