learning_ai_clock/ios/ChronoMindMac/Views/MenuBarPopover.swift

265 lines
8.3 KiB
Swift

// 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
}
}
}