Files
KisaniCal/KisaniCal/Views/SettingsView.swift
kutesir 97782a82bc
Some checks failed
CI / build-and-test (push) Has been cancelled
habits: add a 'Done' notification action + private day-count badge (KC-56)
Follow-up to KC-55's design question: habit reminders (Prayer/Bed/Coffee) stay
out of Today/Matrix/Activity History (would add 3-9 daily checkboxes forever,
against 'nothing shouts'), but now get a 'Done' action on the notification
itself -- like the workout confirm flow's 'Yes, log it' -- that logs a private
timestamp.

New HABIT_LOG category/HABIT_DONE action; every habit notification carries
userInfo['habitType'] (bed/prayer/coffee -- slot detail collapses to the type).
logHabitDone/habitDoneCount store a deduped date array in UserDefaults.kisani,
mirroring the lifetime-tally streak philosophy from KC-40 rather than a
breakable streak. Settings shows a quiet 'Xd' badge per habit -- the only
place the count appears; nothing touches the task list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 02:12:16 +03:00

1650 lines
92 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
extension Bundle {
/// Public marketing version, e.g. "2.0".
var marketingVersion: String { infoDictionary?["CFBundleShortVersionString"] as? String ?? "" }
/// Build number, e.g. "9".
var buildNumber: String { infoDictionary?["CFBundleVersion"] as? String ?? "" }
/// Combined display, e.g. "2.0 (9)" single source of truth for any UI showing the version.
var versionDisplay: String { buildNumber.isEmpty ? marketingVersion : "\(marketingVersion) (\(buildNumber))" }
}
struct SettingsView: View {
@Environment(\.colorScheme) var cs
@EnvironmentObject var workoutVM: WorkoutViewModel
@EnvironmentObject var taskVM: TaskViewModel
@ObservedObject private var auth = AuthManager.shared
@ObservedObject private var calStore = CalendarStore.shared
// Persisted settings
@AppStorage("appearanceMode") private var appearanceMode = 0
@AppStorage("soundsOn") private var soundsOn = true
@AppStorage("showMatrixTab") private var showMatrixTab = true
@AppStorage("showWorkoutTab") private var showWorkoutTab = true
@AppStorage("dateFormat") private var dateFormat = 0
@AppStorage("firstDayMonday") private var firstDayMonday = false
@AppStorage("showCompleted") private var showCompleted = false
@AppStorage("weightUnit") private var weightUnit = 0
@AppStorage("defaultSets") private var defaultSets = 3
@AppStorage("defaultReps") private var defaultReps = 10
@AppStorage("iCloudSync") private var iCloudSync = true
@AppStorage("googleCalSync") private var googleCalSync = false
@AppStorage("iCloudCalSync") private var iCloudCalSync = true
@AppStorage("streakGoal") private var streakGoal = 5
// App-Group store so NotificationManager (which reads UserDefaults.kisani) sees it.
@AppStorage("kisani.periodMilestones", store: UserDefaults.kisani) private var periodMilestones = true
@AppStorage("bedReminderEnabled", store: UserDefaults.kisani) private var bedReminderEnabled = false
@AppStorage("prayerReminderEnabled", store: UserDefaults.kisani) private var prayerReminderEnabled = false
@AppStorage("coffeeReminderEnabled", store: UserDefaults.kisani) private var coffeeReminderEnabled = false
// Sheet presentation
@State private var showProfile = false
@State private var showTabBar = false
@State private var showAppearance = false
@State private var showDateTime = false
@State private var showWidgets = false
@State private var showGeneral = false
@State private var showImport = false
@State private var showCalendar = false
@State private var showBackup = false
@State private var showWorkoutSettings = false
@State private var showHabitReminders = false
@State private var showRestTimer = false
@State private var showHelp = false
@State private var showFollow = false
@State private var showAbout = false
private var appearanceLabel: String { ["System", "Light", "Dark"][appearanceMode] }
private var restLabel: String { workoutVM.restTimerEnabled ? "\(workoutVM.restTimerSeconds)s" : "Off" }
private var weightLabel: String { weightUnit == 0 ? "kg" : "lbs" }
private var dateLabel: String { ["MMM d", "MM/DD", "DD/MM"][dateFormat] }
private var habitsLabel: String? {
let on = [bedReminderEnabled, prayerReminderEnabled, coffeeReminderEnabled].filter { $0 }.count
return on == 0 ? nil : "\(on) on"
}
var body: some View {
ZStack {
AppColors.background(cs).ignoresSafeArea()
ScrollView(showsIndicators: false) {
VStack(spacing: 0) {
Color.clear.frame(height: 0).trackScrollForTabBar()
Text("Settings")
.font(AppFonts.sans(15, weight: .semibold))
.foregroundColor(AppColors.text(cs))
.padding(.top, 10).padding(.bottom, 4)
// Profile
Button { showProfile = true } label: {
HStack(spacing: 12) {
ZStack {
Circle().fill(AppColors.accentSoft)
Text(initials(for: auth.currentUser?.displayName))
.font(AppFonts.sans(15, weight: .bold))
.foregroundColor(AppColors.accent)
}
.frame(width: 44, height: 44)
.overlay(Circle().stroke(AppColors.accent.opacity(0.18), lineWidth: 1.5))
VStack(alignment: .leading, spacing: 5) {
Text(auth.currentUser?.displayName ?? "My Account")
.font(AppFonts.sans(14, weight: .semibold))
.foregroundColor(AppColors.text(cs))
HStack(spacing: 5) {
AppBadge(
text: workoutVM.streakDays > 0 ? "\(workoutVM.streakDays)d \(streakBars(workoutVM.streakDays, goal: streakGoal))" : "no streak",
bg: AppColors.accentSoft, fg: AppColors.accent
)
AppBadge(
text: "\(taskVM.tasks.filter { $0.isComplete }.count) done",
bg: AppColors.greenSoft, fg: AppColors.green
)
AppBadge(
text: weightUnit == 0 ? "kg" : "lbs",
bg: AppColors.surface3(cs), fg: AppColors.text2(cs)
)
}
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 10))
.foregroundColor(AppColors.text3(cs))
}
.padding(14).cardStyle()
}
.buttonStyle(.plain)
.padding(.horizontal, 14).padding(.top, 10).padding(.bottom, 14)
// CUSTOMIZE
SttSection(label: "Customize") {
SttNavRow(icon: "menucard", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Tab Bar", value: "\(showMatrixTab && showWorkoutTab ? "5" : showMatrixTab || showWorkoutTab ? "4" : "3") tabs") { showTabBar = true }
SttNavRow(icon: "paintpalette", bg: AppColors.blueSoft, fg: AppColors.blue, label: "Appearance", value: appearanceLabel) { showAppearance = true }
SttToggleRow(icon: "bell", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Sounds & Notifications", isOn: $soundsOn)
SttToggleRow(icon: "calendar.badge.clock", bg: AppColors.blueSoft, fg: AppColors.blue, label: "Time Milestones",
isOn: Binding(
get: { periodMilestones },
set: { periodMilestones = $0
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM) }))
SttNavRow(icon: "clock", bg: AppColors.greenSoft, fg: AppColors.green, label: "Date & Time", value: dateLabel) { showDateTime = true }
SttNavRow(icon: "puzzlepiece", bg: AppColors.yellowSoft, fg: AppColors.yellow, label: "Widgets") { showWidgets = true }
SttNavRow(icon: "gearshape", bg: AppColors.surface3(cs), label: "General", isLast: true) { showGeneral = true }
}
// DATA
SttSection(label: "Data") {
SttNavRow(icon: "calendar", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Calendar", value: calStore.isAuthorized ? "● Connected" : (calStore.isDenied ? "Denied" : "Connect"), valueColor: calStore.isAuthorized ? AppColors.green : nil) { showCalendar = true }
SttNavRow(icon: "arrow.up.right", bg: AppColors.greenSoft, fg: AppColors.green, label: "Connected Sources", value: googleCalSync || iCloudCalSync ? "Connected" : nil) { showImport = true }
SttNavRow(icon: "cloud", bg: AppColors.blueSoft, fg: AppColors.blue, label: "Library Sync", value: iCloudSync ? "● Synced" : "Off", valueColor: iCloudSync ? AppColors.green : nil, isLast: true) { showBackup = true }
}
// FITNESS
SttSection(label: "Fitness") {
SttNavRow(icon: "dumbbell", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Workout Settings", value: weightLabel) { showWorkoutSettings = true }
SttNavRow(icon: "figure.run", bg: AppColors.accentSoft, fg: AppColors.accent, label: "Rest Timer", value: restLabel) { showRestTimer = true }
HealthToggleRow(isLast: true)
}
// HABITS
SttSection(label: "Habits") {
SttNavRow(icon: "hands.sparkles", bg: AppColors.yellowSoft, fg: AppColors.yellow,
label: "Habit Reminders", value: habitsLabel,
valueColor: habitsLabel != nil ? AppColors.green : nil, isLast: true) { showHabitReminders = true }
}
// ACCOUNT
SttSection(label: "Account", secondary: true) {
if let user = AuthManager.shared.currentUser {
SttNavRow(icon: "person.circle", bg: AppColors.surface3(cs),
label: user.displayName ?? "Apple Account",
value: user.email, isLast: false) { }
}
Button {
AuthManager.shared.signOut()
} label: {
HStack(spacing: 14) {
ZStack {
RoundedRectangle(cornerRadius: 8).fill(AppColors.redSoft).frame(width: 32, height: 32)
Image(systemName: "rectangle.portrait.and.arrow.right")
.font(.system(size: 14, weight: .medium))
.foregroundColor(AppColors.red)
}
Text("Sign Out")
.font(AppFonts.sans(15))
.foregroundColor(AppColors.red)
Spacer()
}
.padding(.horizontal, 16).frame(height: 52)
}
.buttonStyle(.plain)
}
// SUPPORT
SttSection(label: "Support", secondary: true) {
SttNavRow(icon: "star", bg: AppColors.yellowSoft, fg: AppColors.yellow, label: "Help & Feedback") { showHelp = true }
SttNavRow(icon: "message", bg: AppColors.blueSoft, fg: AppColors.blue, label: "Follow Us") { showFollow = true }
SttNavRow(icon: "info.circle", bg: AppColors.surface3(cs), label: "About", value: "v\(Bundle.main.marketingVersion)", isLast: true) { showAbout = true }
}
Spacer().frame(height: 40)
}
}
}
.sheet(isPresented: $showProfile) { ProfileStatsSheet().environmentObject(taskVM).environmentObject(workoutVM).presentationDetents([.large]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showTabBar) { TabBarSheet(showMatrix: $showMatrixTab, showWorkout: $showWorkoutTab).presentationDetents([.height(320)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showAppearance) { AppearanceSheet(mode: $appearanceMode).presentationDetents([.height(290)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showDateTime) { DateTimeSheet(format: $dateFormat, mondayFirst: $firstDayMonday).presentationDetents([.height(300)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showWidgets) { WidgetsSheet().presentationDetents([.height(320)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showGeneral) { GeneralSheet(showCompleted: $showCompleted, mondayFirst: $firstDayMonday).presentationDetents([.height(260)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showImport) { ImportSheet(googleCal: $googleCalSync, iCloudCal: $iCloudCalSync).presentationDetents([.height(280)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showCalendar) { CalendarConnectSheet(calStore: calStore).presentationDetents([.large]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showBackup) { BackupSheet(iCloudSync: $iCloudSync).presentationDetents([.height(280)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showWorkoutSettings) { WorkoutSettingsSheet(unit: $weightUnit, sets: $defaultSets, reps: $defaultReps).environmentObject(workoutVM).presentationDetents([.large]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showHabitReminders) { HabitRemindersSheet().environmentObject(workoutVM).environmentObject(taskVM).presentationDetents([.large]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showRestTimer) { RestTimerSheet(seconds: $workoutVM.restTimerSeconds, enabled: $workoutVM.restTimerEnabled).presentationDetents([.height(360)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showHelp) { HelpSheet().presentationDetents([.height(300)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showFollow) { FollowSheet().presentationDetents([.height(280)]).presentationDragIndicator(.visible) }
.sheet(isPresented: $showAbout) { AboutSheet().presentationDetents([.height(340)]).presentationDragIndicator(.visible) }
.onAppear { calStore.refreshStatus() }
}
}
// MARK: - Health Toggle Row (requests auth when turned on)
private struct HealthToggleRow: View {
@Environment(\.colorScheme) private var cs
var isLast: Bool = false
@ObservedObject private var hk = HealthKitManager.shared
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 11) {
Image(systemName: "heart.fill")
.font(.system(size: 14))
.foregroundColor(AppColors.green)
.frame(width: 28, height: 28)
.background(AppColors.greenSoft)
.cornerRadius(8)
VStack(alignment: .leading, spacing: 1) {
Text("Health Integration")
.font(AppFonts.sans(13))
.foregroundColor(AppColors.text(cs))
if hk.authorized {
Text("Connected")
.font(AppFonts.mono(9))
.foregroundColor(AppColors.green)
} else if !hk.isAvailable {
Text("Not available on this device")
.font(AppFonts.mono(9))
.foregroundColor(AppColors.text3(cs))
}
}
Spacer()
Toggle("", isOn: Binding(
get: { hk.authorized },
set: { on in
if on { Task { _ = await hk.requestAuthorization() } }
else { hk.disconnect() }
}
))
.labelsHidden()
.tint(AppColors.green)
}
.padding(.horizontal, 14).padding(.vertical, 11)
if !isLast { Divider().background(AppColors.border(cs)).padding(.leading, 53) }
}
}
}
// MARK: - Section
private struct SttSection<Content: View>: View {
@Environment(\.colorScheme) private var cs
let label: String
var secondary: Bool = false
@ViewBuilder let content: Content
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text(label)
.font(AppFonts.sans(secondary ? 9.5 : 10, weight: .semibold))
.foregroundColor(secondary ? AppColors.text3(cs) : AppColors.text2(cs))
.padding(.horizontal, 18).padding(.bottom, 5)
VStack(spacing: 0) { content }
.cardStyle().padding(.horizontal, 14).padding(.bottom, secondary ? 6 : 8)
}
}
}
// MARK: - Nav Row
private struct SttNavRow: View {
@Environment(\.colorScheme) private var cs
let icon: String; let bg: Color
var fg: Color? = nil
let label: String
var value: String? = nil; var valueColor: Color? = nil; var isLast: Bool = false
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(spacing: 0) {
HStack(spacing: 11) {
Image(systemName: icon).font(.system(size: 14))
.foregroundColor(fg ?? AppColors.text2(cs))
.frame(width: 28, height: 28).background(bg).cornerRadius(8)
Text(label).font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
if let v = value {
Text(v).font(AppFonts.mono(11))
.foregroundColor(valueColor ?? AppColors.text2(cs)).lineLimit(1)
}
Image(systemName: "chevron.right").font(.system(size: 10))
.foregroundColor(AppColors.text3(cs))
}
.padding(.horizontal, 14).padding(.vertical, 11)
if !isLast { Divider().background(AppColors.border(cs)).padding(.leading, 53) }
}.contentShape(Rectangle())
}.buttonStyle(.plain)
}
}
// MARK: - Toggle Row
private struct SttToggleRow: View {
@Environment(\.colorScheme) private var cs
let icon: String; let bg: Color
var fg: Color? = nil
let label: String
@Binding var isOn: Bool; var isLast: Bool = false
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 11) {
Image(systemName: icon).font(.system(size: 14))
.foregroundColor(fg ?? AppColors.text2(cs))
.frame(width: 28, height: 28).background(bg).cornerRadius(8)
Text(label).font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $isOn).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 11)
if !isLast { Divider().background(AppColors.border(cs)).padding(.leading, 53) }
}
}
}
// MARK: - Shared Header
private struct SheetHeader: View {
@Environment(\.colorScheme) private var cs
let title: String; let onDone: () -> Void
var body: some View {
HStack {
Text(title).font(AppFonts.sans(17, weight: .bold)).foregroundColor(AppColors.text(cs))
Spacer()
Button("Done", action: onDone).font(AppFonts.sans(13, weight: .semibold))
.foregroundColor(AppColors.accent).buttonStyle(.plain)
}
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 16)
}
}
// MARK: - Tab Bar Sheet
private struct TabBarSheet: View {
@Binding var showMatrix: Bool
@Binding var showWorkout: Bool
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var cs
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Tab Bar") { dismiss() }
VStack(spacing: 0) {
TabToggleRow(icon: "checkmark", label: "Tasks", isOn: .constant(true), locked: true)
TabToggleRow(icon: "calendar", label: "Calendar", isOn: .constant(true), locked: true)
TabToggleRow(icon: "square.grid.2x2", label: "Matrix", isOn: $showMatrix, locked: false)
TabToggleRow(icon: "dumbbell", label: "Workout", isOn: $showWorkout, locked: false)
TabToggleRow(icon: "gearshape", label: "Settings", isOn: .constant(true), locked: true, isLast: true)
}
.cardStyle().padding(.horizontal, 20)
Text("Tasks, Calendar, and Settings are always visible.")
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
.multilineTextAlignment(.center).padding(.horizontal, 20).padding(.top, 12)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
private struct TabToggleRow: View {
@Environment(\.colorScheme) private var cs
let icon: String; let label: String
@Binding var isOn: Bool; let locked: Bool; var isLast: Bool = false
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 12) {
Image(systemName: icon).font(.system(size: 13)).foregroundColor(locked ? AppColors.text3(cs) : AppColors.text(cs)).frame(width: 22)
Text(label).font(AppFonts.sans(13)).foregroundColor(locked ? AppColors.text3(cs) : AppColors.text(cs))
Spacer()
if locked {
Text("Always on").font(AppFonts.mono(9.5)).foregroundColor(AppColors.text3(cs))
} else {
Toggle("", isOn: $isOn).labelsHidden().tint(AppColors.accent)
}
}
.padding(.horizontal, 14).padding(.vertical, 12)
if !isLast { Divider().background(AppColors.border(cs)).padding(.leading, 48) }
}
}
}
// MARK: - Appearance Sheet
private struct AppearanceSheet: View {
@Binding var mode: Int
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
private let options = [("System", "circle.lefthalf.filled"), ("Light", "sun.max"), ("Dark", "moon.stars")]
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Appearance") { dismiss() }
VStack(spacing: 8) {
ForEach(options.indices, id: \.self) { i in
Button { withAnimation(KisaniSpring.micro) { mode = i } } label: {
HStack(spacing: 14) {
Image(systemName: options[i].1).font(.system(size: 15, weight: .medium))
.foregroundColor(mode == i ? AppColors.accent : AppColors.text2(cs)).frame(width: 24)
Text(options[i].0).font(AppFonts.sans(14, weight: mode == i ? .semibold : .regular))
.foregroundColor(AppColors.text(cs))
Spacer()
if mode == i { Image(systemName: "checkmark").font(.system(size: 12, weight: .bold)).foregroundColor(AppColors.accent) }
}
.padding(.horizontal, 16).padding(.vertical, 14)
.background(mode == i ? AppColors.accentSoft : AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.medium))
.overlay(RoundedRectangle(cornerRadius: AppRadius.medium).stroke(mode == i ? AppColors.accent.opacity(0.35) : AppColors.border(cs), lineWidth: 1))
}.buttonStyle(.plain)
}
}
.padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Date & Time Sheet
private struct DateTimeSheet: View {
@Binding var format: Int
@Binding var mondayFirst: Bool
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
private let formats = ["MMM d", "MM/DD/YYYY", "DD/MM/YYYY"]
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Date & Time") { dismiss() }
VStack(alignment: .leading, spacing: 20) {
VStack(alignment: .leading, spacing: 8) {
Text("DATE FORMAT").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
HStack(spacing: 8) {
ForEach(formats.indices, id: \.self) { i in
Button { withAnimation { format = i } } label: {
Text(formats[i]).font(AppFonts.mono(11, weight: .bold))
.foregroundColor(format == i ? AppColors.accent : AppColors.text2(cs))
.frame(maxWidth: .infinity).padding(.vertical, 10)
.background(format == i ? AppColors.accentSoft : AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).stroke(format == i ? AppColors.accent : AppColors.border(cs), lineWidth: 1))
}.buttonStyle(.plain)
}
}
}
VStack(spacing: 0) {
HStack {
Text("First day of week").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Text(mondayFirst ? "Monday" : "Sunday").font(AppFonts.mono(11)).foregroundColor(AppColors.text3(cs))
Toggle("", isOn: $mondayFirst).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
}
.cardStyle()
}
.padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Widgets Sheet
private struct WidgetsSheet: View {
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
private let widgets = [("Small", "1×1 — Today's task count", "square.grid.2x2"), ("Medium", "2×1 — Upcoming tasks list", "rectangle.grid.1x2"), ("Large", "2×2 — Full task view", "square.split.2x2")]
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Widgets") { dismiss() }
VStack(spacing: 10) {
ForEach(widgets, id: \.0) { w in
HStack(spacing: 14) {
Image(systemName: w.2).font(.system(size: 20)).foregroundColor(AppColors.accent)
.frame(width: 44, height: 44).background(AppColors.accentSoft)
.clipShape(RoundedRectangle(cornerRadius: 10))
VStack(alignment: .leading, spacing: 2) {
Text(w.0).font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.text(cs))
Text(w.1).font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
}
Spacer()
}
.padding(12).cardStyle()
}
Text("Long-press your home screen → tap + → search Wenza to add widgets.")
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
.multilineTextAlignment(.center).padding(.top, 4)
}
.padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - General Sheet
private struct GeneralSheet: View {
@Binding var showCompleted: Bool
@Binding var mondayFirst: Bool
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "General") { dismiss() }
VStack(spacing: 0) {
HStack {
Text("Show completed tasks").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $showCompleted).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
Divider().background(AppColors.border(cs)).padding(.leading, 14)
HStack {
Text("Week starts on Monday").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $mondayFirst).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
}
.cardStyle().padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Import Sheet
private struct ImportSheet: View {
@Binding var googleCal: Bool
@Binding var iCloudCal: Bool
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Import & Integration") { dismiss() }
VStack(spacing: 0) {
HStack(spacing: 12) {
Image(systemName: "calendar.badge.plus").font(.system(size: 16))
.foregroundColor(AppColors.blue).frame(width: 28, height: 28)
.background(AppColors.blueSoft).cornerRadius(8)
Text("Google Calendar").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $googleCal).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
Divider().background(AppColors.border(cs)).padding(.leading, 53)
HStack(spacing: 12) {
Image(systemName: "cloud").font(.system(size: 16))
.foregroundColor(AppColors.blue).frame(width: 28, height: 28)
.background(AppColors.blueSoft).cornerRadius(8)
Text("iCloud Calendar").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $iCloudCal).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
}
.cardStyle().padding(.horizontal, 20)
Text("Enabling integrations will sync events to your Wenza calendar.")
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
.multilineTextAlignment(.center).padding(.horizontal, 20).padding(.top, 12)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Backup Sheet
private struct BackupSheet: View {
@Binding var iCloudSync: Bool
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Backup & Sync") { dismiss() }
VStack(spacing: 14) {
VStack(spacing: 0) {
HStack(spacing: 12) {
Image(systemName: "icloud").font(.system(size: 16))
.foregroundColor(AppColors.blue).frame(width: 28, height: 28)
.background(AppColors.blueSoft).cornerRadius(8)
Text("iCloud Sync").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $iCloudSync).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
if iCloudSync {
Divider().background(AppColors.border(cs)).padding(.leading, 53)
HStack {
Text("Last synced").font(AppFonts.sans(12)).foregroundColor(AppColors.text2(cs))
Spacer()
Text("Today · just now").font(AppFonts.mono(11)).foregroundColor(AppColors.green)
}
.padding(.horizontal, 14).padding(.vertical, 10)
}
}
.cardStyle().padding(.horizontal, 20)
.animation(KisaniSpring.snappy, value: iCloudSync)
if iCloudSync {
Button {
// trigger sync
} label: {
Text("Sync Now")
.font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.accent)
.frame(maxWidth: .infinity).padding(13)
.overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.accent, lineWidth: 1.5))
}
.buttonStyle(.plain).padding(.horizontal, 20)
}
}
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Workout Settings Sheet
private struct WorkoutSettingsSheet: View {
@EnvironmentObject var workoutVM: WorkoutViewModel
@Binding var unit: Int
@Binding var sets: Int
@Binding var reps: Int
@AppStorage("bodyWeightKg") private var bodyWeightKg: Double = 80
@AppStorage("heightCm") private var heightCm: Double = 175
@AppStorage("workoutReminderEnabled") private var workoutReminderEnabled: Bool = true
@AppStorage("workoutHour") private var workoutHour: Int = 18
@AppStorage("workoutMinute") private var workoutMinute: Int = 0
@AppStorage("workoutCheckInEnabled") private var workoutCheckInEnabled: Bool = false
@AppStorage("workoutConfirmEnabled") private var workoutConfirmEnabled: Bool = false
@AppStorage("workoutConfirmHour") private var workoutConfirmHour: Int = 20
@AppStorage("workoutConfirmMinute") private var workoutConfirmMinute: Int = 0
@AppStorage("checkInHour") private var checkInHour: Int = 19
@AppStorage("checkInMinute") private var checkInMinute: Int = 30
@AppStorage("streakGoal") private var streakGoal: Int = 5
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
@State private var bwStr = ""
@State private var htStr = ""
@State private var workoutTime: Date = Date()
@State private var checkInTime: Date = Date()
@State private var confirmTime: Date = Date()
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Workout Settings") { dismiss() }
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 20) {
// Reminders
VStack(alignment: .leading, spacing: 8) {
Text("REMINDERS").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
VStack(spacing: 0) {
// Pre-workout
HStack(spacing: 11) {
Image(systemName: "bell.fill")
.font(.system(size: 13)).foregroundColor(AppColors.accent)
.frame(width: 28, height: 28).background(AppColors.accentSoft).cornerRadius(8)
Text("Pre-workout reminder").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $workoutReminderEnabled).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 11)
if workoutReminderEnabled {
Divider().background(AppColors.border(cs)).padding(.leading, 53)
HStack {
Text("Remind me at")
.font(AppFonts.sans(12)).foregroundColor(AppColors.text2(cs))
Spacer()
DatePicker("", selection: $workoutTime, displayedComponents: .hourAndMinute)
.labelsHidden().tint(AppColors.accent)
.onChange(of: workoutTime) { v in
let c = Calendar.current.dateComponents([.hour, .minute], from: v)
workoutHour = c.hour ?? 18
workoutMinute = c.minute ?? 0
}
}
.padding(.horizontal, 14).padding(.vertical, 8)
}
Divider().background(AppColors.border(cs)).padding(.leading, 53)
// Post-workout check-in
HStack(spacing: 11) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 13)).foregroundColor(AppColors.green)
.frame(width: 28, height: 28).background(AppColors.greenSoft).cornerRadius(8)
Text("Post-workout check-in").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $workoutCheckInEnabled).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 11)
if workoutCheckInEnabled {
Divider().background(AppColors.border(cs)).padding(.leading, 53)
HStack {
Text("Check in at")
.font(AppFonts.sans(12)).foregroundColor(AppColors.text2(cs))
Spacer()
DatePicker("", selection: $checkInTime, displayedComponents: .hourAndMinute)
.labelsHidden().tint(AppColors.accent)
.onChange(of: checkInTime) { v in
let c = Calendar.current.dateComponents([.hour, .minute], from: v)
checkInHour = c.hour ?? 19
checkInMinute = c.minute ?? 30
}
}
.padding(.horizontal, 14).padding(.vertical, 8)
}
}
.cardStyle()
.animation(KisaniSpring.snappy, value: workoutReminderEnabled)
.animation(KisaniSpring.snappy, value: workoutCheckInEnabled)
}
// Workout Confirmation
VStack(alignment: .leading, spacing: 8) {
Text("HEALTH SYNC").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
VStack(spacing: 0) {
HStack(spacing: 11) {
Image(systemName: "figure.strengthtraining.traditional")
.font(.system(size: 13)).foregroundColor(.white)
.frame(width: 28, height: 28).background(AppColors.blue).cornerRadius(8)
VStack(alignment: .leading, spacing: 1) {
Text("Sync streak from Apple Health").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Text("Import past workouts into your streak")
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
}
Spacer()
Button {
Task { await workoutVM.syncFromHealthKit() }
} label: {
Text("Sync now")
.font(AppFonts.mono(10, weight: .bold))
.foregroundColor(AppColors.blue)
.padding(.horizontal, 10).padding(.vertical, 5)
.background(AppColors.blueSoft)
.clipShape(Capsule())
}
.buttonStyle(.plain)
}
.padding(.horizontal, 14).padding(.vertical, 12)
Divider().background(AppColors.border(cs)).padding(.leading, 53)
HStack(spacing: 11) {
Image(systemName: "bell.badge.fill")
.font(.system(size: 13)).foregroundColor(.white)
.frame(width: 28, height: 28).background(AppColors.accent).cornerRadius(8)
Text("Workout confirmation reminder").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $workoutConfirmEnabled).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 11)
if workoutConfirmEnabled {
Divider().background(AppColors.border(cs)).padding(.leading, 53)
HStack {
Text("Ask me at")
.font(AppFonts.sans(12)).foregroundColor(AppColors.text2(cs))
Spacer()
DatePicker("", selection: $confirmTime, displayedComponents: .hourAndMinute)
.labelsHidden().tint(AppColors.accent)
.onChange(of: confirmTime) { v in
let c = Calendar.current.dateComponents([.hour, .minute], from: v)
workoutConfirmHour = c.hour ?? 20
workoutConfirmMinute = c.minute ?? 0
}
}
.padding(.horizontal, 14).padding(.vertical, 8)
}
}
.cardStyle()
.animation(KisaniSpring.snappy, value: workoutConfirmEnabled)
}
// Streak Goal
VStack(alignment: .leading, spacing: 8) {
Text("STREAK GOAL").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
HStack(spacing: 7) {
ForEach([3, 4, 5, 6, 7], id: \.self) { days in
Button { withAnimation(KisaniSpring.micro) { streakGoal = days } } label: {
VStack(spacing: 4) {
Text("\(days)")
.font(AppFonts.mono(15, weight: .bold))
.foregroundColor(streakGoal == days ? AppColors.accent : AppColors.text2(cs))
Text("days")
.font(AppFonts.mono(8))
.foregroundColor(streakGoal == days ? AppColors.accent : AppColors.text3(cs))
}
.frame(maxWidth: .infinity).padding(.vertical, 10)
.background(streakGoal == days ? AppColors.accentSoft : AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: 9))
.overlay(RoundedRectangle(cornerRadius: 9).stroke(
streakGoal == days ? AppColors.accent : AppColors.border(cs),
lineWidth: streakGoal == days ? 1.5 : 1
))
}.buttonStyle(.plain)
}
}
Text("Your weekly workout target. Every workout you log adds to your total — a missed day never takes it away.")
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
}
// Body Profile
VStack(alignment: .leading, spacing: 8) {
Text("BODY PROFILE").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
VStack(spacing: 0) {
HStack {
Text("Body Weight").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
HStack(spacing: 4) {
TextField("80", text: $bwStr)
.font(AppFonts.mono(13, weight: .semibold))
.foregroundColor(AppColors.text(cs))
.multilineTextAlignment(.trailing)
.keyboardType(.decimalPad)
.frame(width: 56)
.onChange(of: bwStr) { val in
let clean = String(val.filter { $0.isNumber || $0 == "." }.prefix(5))
if clean != val { bwStr = clean }
if let d = Double(clean) { bodyWeightKg = d }
}
Text("kg")
.font(AppFonts.mono(10, weight: .bold))
.foregroundColor(AppColors.text3(cs))
}
}
.padding(.horizontal, 14).padding(.vertical, 11)
Divider().background(AppColors.border(cs)).padding(.leading, 14)
HStack {
Text("Height").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
HStack(spacing: 4) {
TextField("175", text: $htStr)
.font(AppFonts.mono(13, weight: .semibold))
.foregroundColor(AppColors.text(cs))
.multilineTextAlignment(.trailing)
.keyboardType(.numberPad)
.frame(width: 56)
.onChange(of: htStr) { val in
let clean = String(val.filter { $0.isNumber }.prefix(3))
if clean != val { htStr = clean }
if let d = Double(clean) { heightCm = d }
}
Text("cm")
.font(AppFonts.mono(10, weight: .bold))
.foregroundColor(AppColors.text3(cs))
}
}
.padding(.horizontal, 14).padding(.vertical, 11)
}
.cardStyle()
}
// Weight Unit
VStack(alignment: .leading, spacing: 8) {
Text("WEIGHT UNIT").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
HStack(spacing: 10) {
ForEach(["kg", "lbs"].indices, id: \.self) { i in
Button { withAnimation(KisaniSpring.micro) { unit = i } } label: {
Text(["kg", "lbs"][i]).font(AppFonts.mono(13, weight: .bold))
.foregroundColor(unit == i ? AppColors.accent : AppColors.text2(cs))
.frame(maxWidth: .infinity).padding(.vertical, 12)
.background(unit == i ? AppColors.accentSoft : AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: 9))
.overlay(RoundedRectangle(cornerRadius: 9).stroke(unit == i ? AppColors.accent : AppColors.border(cs), lineWidth: unit == i ? 1.5 : 1))
}.buttonStyle(.plain)
}
}
}
// Defaults
VStack(spacing: 0) {
StepperRow(label: "Default Sets", value: $sets, range: 1...10)
Divider().background(AppColors.border(cs)).padding(.leading, 14)
StepperRow(label: "Default Reps", value: $reps, range: 1...30, isLast: true)
}
.cardStyle()
}
.padding(.horizontal, 20)
.padding(.bottom, 20)
}
}
.background(AppColors.background(cs).ignoresSafeArea())
.onAppear {
bwStr = bodyWeightKg > 0 ? (bodyWeightKg.truncatingRemainder(dividingBy: 1) == 0 ? "\(Int(bodyWeightKg))" : String(format: "%.1f", bodyWeightKg)) : ""
htStr = heightCm > 0 ? "\(Int(heightCm))" : ""
let today = Calendar.current.dateComponents([.year, .month, .day], from: Date())
var wc = today; wc.hour = workoutHour; wc.minute = workoutMinute
workoutTime = Calendar.current.date(from: wc) ?? Date()
var cc = today; cc.hour = checkInHour; cc.minute = checkInMinute
checkInTime = Calendar.current.date(from: cc) ?? Date()
var cf = today; cf.hour = workoutConfirmHour; cf.minute = workoutConfirmMinute
confirmTime = Calendar.current.date(from: cf) ?? Date()
}
}
}
// MARK: - Habit Reminders Sheet
//
// Three independent daily reminders: prayer (up to 4 named times a day),
// bed-making (one nudge research says it's the easiest first win of the
// day), and coffee/caffeine (up to 4 named times plus one custom reminder).
// All keys use the App Group store (`UserDefaults.kisani`) the same store
// NotificationManager reads from so toggling here actually schedules.
private struct HabitRemindersSheet: View {
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
@EnvironmentObject var workoutVM: WorkoutViewModel
@EnvironmentObject var taskVM: TaskViewModel
@AppStorage("bedReminderEnabled", store: UserDefaults.kisani) private var bedReminderEnabled = false
@AppStorage("bedReminderHour", store: UserDefaults.kisani) private var bedReminderHour = 8
@AppStorage("bedReminderMinute", store: UserDefaults.kisani) private var bedReminderMinute = 0
@AppStorage("prayerReminderEnabled", store: UserDefaults.kisani) private var prayerReminderEnabled = false
@AppStorage("coffeeReminderEnabled", store: UserDefaults.kisani) private var coffeeReminderEnabled = false
@AppStorage("coffeeCustomEnabled", store: UserDefaults.kisani) private var coffeeCustomEnabled = false
@AppStorage("coffeeCustomLabel", store: UserDefaults.kisani) private var coffeeCustomLabel = "Coffee"
@AppStorage("coffeeCustomHour", store: UserDefaults.kisani) private var coffeeCustomHour = 16
@AppStorage("coffeeCustomMinute", store: UserDefaults.kisani) private var coffeeCustomMinute = 30
private let prayerSlots = ["Morning", "Afternoon", "Evening", "Night"]
private let coffeeSlots = ["Morning", "Midday", "Afternoon", "Evening"]
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Habit Reminders") {
dismiss()
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
}
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 20) {
// Prayer
VStack(alignment: .leading, spacing: 8) {
Text("PRAYER").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
VStack(spacing: 0) {
HStack(spacing: 11) {
Image(systemName: "hands.sparkles.fill")
.font(.system(size: 13)).foregroundColor(AppColors.yellow)
.frame(width: 28, height: 28).background(AppColors.yellowSoft).cornerRadius(8)
VStack(alignment: .leading, spacing: 1) {
HStack(spacing: 6) {
Text("Prayer reminders").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
habitBadge("prayer")
}
Text("\"Have you had a chance to pray and give thanks?\"")
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
}
Spacer()
Toggle("", isOn: $prayerReminderEnabled).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
if prayerReminderEnabled {
ForEach(prayerSlots, id: \.self) { slot in
HabitSlotRow(keyPrefix: "prayer\(slot)", label: slot,
defaultHour: Self.defaultHour(for: slot, prayer: true),
defaultMinute: 0, defaultEnabled: true)
}
}
}
.cardStyle()
.animation(KisaniSpring.snappy, value: prayerReminderEnabled)
}
// Bed-making
VStack(alignment: .leading, spacing: 8) {
Text("MORNING").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
VStack(spacing: 0) {
HStack(spacing: 11) {
Image(systemName: "bed.double.fill")
.font(.system(size: 13)).foregroundColor(AppColors.blue)
.frame(width: 28, height: 28).background(AppColors.blueSoft).cornerRadius(8)
VStack(alignment: .leading, spacing: 1) {
HStack(spacing: 6) {
Text("Make your bed").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
habitBadge("bed")
}
Text("The simplest task that starts the day with a win.")
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
}
Spacer()
Toggle("", isOn: $bedReminderEnabled).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
if bedReminderEnabled {
Divider().background(AppColors.border(cs)).padding(.leading, 53)
HabitTimeRow(label: "Remind me at", hour: $bedReminderHour, minute: $bedReminderMinute)
}
}
.cardStyle()
.animation(KisaniSpring.snappy, value: bedReminderEnabled)
}
// Coffee / Caffeine
VStack(alignment: .leading, spacing: 8) {
Text("CAFFEINE").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
VStack(spacing: 0) {
HStack(spacing: 11) {
Image(systemName: "cup.and.saucer.fill")
.font(.system(size: 13)).foregroundColor(.white)
.frame(width: 28, height: 28).background(Color(r: 122, g: 84, b: 52)).cornerRadius(8)
VStack(alignment: .leading, spacing: 1) {
HStack(spacing: 6) {
Text("Coffee reminders").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
habitBadge("coffee")
}
Text("Track when you usually reach for a cup.")
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
}
Spacer()
Toggle("", isOn: $coffeeReminderEnabled).labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 12)
if coffeeReminderEnabled {
ForEach(coffeeSlots, id: \.self) { slot in
HabitSlotRow(keyPrefix: "coffee\(slot)", label: slot,
defaultHour: Self.defaultHour(for: slot, prayer: false),
defaultMinute: 0, defaultEnabled: slot == "Morning")
}
Divider().background(AppColors.border(cs)).padding(.leading, 14)
HStack(spacing: 11) {
TextField("Custom label", text: $coffeeCustomLabel)
.font(AppFonts.sans(12.5)).foregroundColor(AppColors.text(cs))
Spacer()
if coffeeCustomEnabled {
DatePicker("", selection: Binding(
get: { Calendar.current.date(bySettingHour: coffeeCustomHour, minute: coffeeCustomMinute, second: 0, of: Date()) ?? Date() },
set: { v in
let c = Calendar.current.dateComponents([.hour, .minute], from: v)
coffeeCustomHour = c.hour ?? coffeeCustomHour
coffeeCustomMinute = c.minute ?? coffeeCustomMinute
}
), displayedComponents: .hourAndMinute)
.labelsHidden().tint(AppColors.accent)
}
Toggle("", isOn: $coffeeCustomEnabled).labelsHidden().tint(AppColors.accent).scaleEffect(0.85)
}
.padding(.horizontal, 14).padding(.vertical, 9)
}
}
.cardStyle()
.animation(KisaniSpring.snappy, value: coffeeReminderEnabled)
}
Text("All reminders are local to this device and never share your responses — they're just gentle nudges.")
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
}
.padding(.horizontal, 20)
.padding(.bottom, 20)
}
}
.background(AppColors.background(cs).ignoresSafeArea())
}
private static func defaultHour(for slot: String, prayer: Bool) -> Int {
let table: [String: Int] = prayer
? ["Morning": 7, "Afternoon": 13, "Evening": 18, "Night": 21]
: ["Morning": 8, "Midday": 12, "Afternoon": 15, "Evening": 18]
return table[slot] ?? 9
}
/// A quiet "X days" badge from tapping "Done" on a habit's notifications
/// a private lifetime tally, never shown in Today/Matrix/Activity History.
@ViewBuilder
private func habitBadge(_ type: String) -> some View {
let count = NotificationManager.shared.habitDoneCount(type: type)
if count > 0 {
AppBadge(text: "\(count)d", bg: AppColors.greenSoft, fg: AppColors.green)
}
}
}
/// One named, independently-toggleable daily time slot (e.g. "Morning") used
/// for both Prayer and Coffee's dense multi-slot lists. Reads/writes
/// `UserDefaults.kisani` directly under `<keyPrefix>Enabled/Hour/Minute` so it
/// stays in lockstep with what `NotificationManager` schedules from.
private struct HabitSlotRow: View {
@Environment(\.colorScheme) private var cs
let keyPrefix: String
let label: String
let defaultHour: Int
let defaultMinute: Int
let defaultEnabled: Bool
@AppStorage private var enabled: Bool
@AppStorage private var hour: Int
@AppStorage private var minute: Int
init(keyPrefix: String, label: String, defaultHour: Int, defaultMinute: Int, defaultEnabled: Bool) {
self.keyPrefix = keyPrefix; self.label = label
self.defaultHour = defaultHour; self.defaultMinute = defaultMinute; self.defaultEnabled = defaultEnabled
_enabled = AppStorage(wrappedValue: defaultEnabled, "\(keyPrefix)Enabled", store: UserDefaults.kisani)
_hour = AppStorage(wrappedValue: defaultHour, "\(keyPrefix)Hour", store: UserDefaults.kisani)
_minute = AppStorage(wrappedValue: defaultMinute, "\(keyPrefix)Minute", store: UserDefaults.kisani)
}
var body: some View {
VStack(spacing: 0) {
Divider().background(AppColors.border(cs)).padding(.leading, 14)
HStack(spacing: 11) {
Text(label).font(AppFonts.sans(12.5)).foregroundColor(AppColors.text2(cs))
Spacer()
if enabled {
DatePicker("", selection: Binding(
get: { Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: Date()) ?? Date() },
set: { v in
let c = Calendar.current.dateComponents([.hour, .minute], from: v)
hour = c.hour ?? hour; minute = c.minute ?? minute
}
), displayedComponents: .hourAndMinute)
.labelsHidden().tint(AppColors.accent)
}
Toggle("", isOn: $enabled).labelsHidden().tint(AppColors.accent).scaleEffect(0.85)
}
.padding(.horizontal, 14).padding(.vertical, 9)
}
}
}
/// A plain "label + time picker" row (no toggle) used where the toggle
/// already lives on the parent row (e.g. bed-making's single time).
private struct HabitTimeRow: View {
@Environment(\.colorScheme) private var cs
let label: String
@Binding var hour: Int
@Binding var minute: Int
var body: some View {
HStack {
Text(label).font(AppFonts.sans(12)).foregroundColor(AppColors.text2(cs))
Spacer()
DatePicker("", selection: Binding(
get: { Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: Date()) ?? Date() },
set: { v in
let c = Calendar.current.dateComponents([.hour, .minute], from: v)
hour = c.hour ?? hour; minute = c.minute ?? minute
}
), displayedComponents: .hourAndMinute)
.labelsHidden().tint(AppColors.accent)
}
.padding(.horizontal, 14).padding(.vertical, 8)
}
}
private struct StepperRow: View {
@Environment(\.colorScheme) private var cs
let label: String; @Binding var value: Int; let range: ClosedRange<Int>; var isLast: Bool = false
var body: some View {
HStack {
Text(label).font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
Spacer()
HStack(spacing: 0) {
Button { if value > range.lowerBound { value -= 1 } } label: {
Image(systemName: "minus").font(.system(size: 13, weight: .semibold))
.foregroundColor(value > range.lowerBound ? AppColors.accent : AppColors.text3(cs))
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
Text("\(value)").font(AppFonts.mono(13, weight: .bold))
.foregroundColor(AppColors.text(cs)).frame(width: 32).multilineTextAlignment(.center)
Button { if value < range.upperBound { value += 1 } } label: {
Image(systemName: "plus").font(.system(size: 13, weight: .semibold))
.foregroundColor(value < range.upperBound ? AppColors.accent : AppColors.text3(cs))
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
}
.background(AppColors.surface2(cs)).clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).stroke(AppColors.border(cs), lineWidth: 1))
}
.padding(.horizontal, 14).padding(.vertical, 10)
}
}
// MARK: - Rest Timer Sheet
private struct RestTimerSheet: View {
@Binding var seconds: Int
@Binding var enabled: Bool
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
private let options = [30, 45, 60, 90, 120, 180]
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Rest Timer") { dismiss() }
// Toggle
HStack {
Text("Auto-start after each set")
.font(AppFonts.sans(14))
.foregroundColor(AppColors.text(cs))
Spacer()
Toggle("", isOn: $enabled)
.labelsHidden()
.tint(AppColors.accent)
}
.padding(.horizontal, 20)
.padding(.bottom, 20)
// Duration picker (dimmed when disabled)
Text(seconds >= 60 ? String(format: "%d:%02d", seconds / 60, seconds % 60) : "\(seconds)s")
.font(AppFonts.mono(52, weight: .bold))
.foregroundColor(AppColors.accent)
.opacity(enabled ? 1 : 0.35)
.padding(.bottom, 20)
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 10), count: 3), spacing: 10) {
ForEach(options, id: \.self) { opt in
Button { withAnimation(KisaniSpring.micro) { seconds = opt } } label: {
Text(opt >= 60 ? String(format: "%d:%02d", opt / 60, opt % 60) : "\(opt)s")
.font(AppFonts.mono(14, weight: .bold))
.foregroundColor(seconds == opt ? AppColors.accent : AppColors.text2(cs))
.frame(maxWidth: .infinity).padding(.vertical, 13)
.background(seconds == opt ? AppColors.accentSoft : AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
.overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(seconds == opt ? AppColors.accent : AppColors.border(cs), lineWidth: 1))
}
.buttonStyle(.plain)
.opacity(enabled ? 1 : 0.35)
.disabled(!enabled)
}
}
.padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Help & Feedback Sheet
private struct HelpSheet: View {
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var cs
@Environment(\.openURL) var openURL
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Help & Feedback") { dismiss() }
VStack(spacing: 10) {
HelpRow(icon: "envelope", label: "Send Feedback", sub: "Report a bug or suggest a feature") {
if let url = URL(string: "mailto:feedback@kisanicaI.app?subject=Wenza%20Feedback") { openURL(url) }
}
HelpRow(icon: "questionmark.circle", label: "FAQ", sub: "Frequently asked questions") {}
HelpRow(icon: "book", label: "User Guide", sub: "Learn how to use Wenza") {}
}
.padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
private struct HelpRow: View {
@Environment(\.colorScheme) private var cs
let icon: String; let label: String; let sub: String; let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 14) {
Image(systemName: icon).font(.system(size: 16)).foregroundColor(AppColors.accent)
.frame(width: 40, height: 40).background(AppColors.accentSoft)
.clipShape(RoundedRectangle(cornerRadius: 10))
VStack(alignment: .leading, spacing: 2) {
Text(label).font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.text(cs))
Text(sub).font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
}
Spacer()
Image(systemName: "chevron.right").font(.system(size: 10)).foregroundColor(AppColors.text3(cs))
}
.padding(12).cardStyle()
}.buttonStyle(.plain)
}
}
// MARK: - Follow Us Sheet
private struct FollowSheet: View {
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var cs
@Environment(\.openURL) var openURL
private let socials: [(String, String, String, String)] = [
("𝕏 Twitter / X", "Share your streaks", "at.circle", "https://x.com"),
("Discord", "Join the community", "bubble.left.and.bubble.right", "https://discord.com"),
("Instagram", "Behind the scenes", "camera", "https://instagram.com"),
]
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "Follow Us") { dismiss() }
VStack(spacing: 8) {
ForEach(socials, id: \.0) { s in
Button { if let url = URL(string: s.3) { openURL(url) } } label: {
HStack(spacing: 14) {
Image(systemName: s.2).font(.system(size: 16)).foregroundColor(AppColors.accent)
.frame(width: 40, height: 40).background(AppColors.accentSoft)
.clipShape(RoundedRectangle(cornerRadius: 10))
VStack(alignment: .leading, spacing: 2) {
Text(s.0).font(AppFonts.sans(13, weight: .semibold)).foregroundColor(AppColors.text(cs))
Text(s.1).font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
}
Spacer()
Image(systemName: "arrow.up.right").font(.system(size: 10)).foregroundColor(AppColors.text3(cs))
}
.padding(12).cardStyle()
}.buttonStyle(.plain)
}
}
.padding(.horizontal, 20)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}
// MARK: - Profile Stats Sheet
private struct ProfileStatsSheet: View {
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var cs
@EnvironmentObject var taskVM: TaskViewModel
@EnvironmentObject var workoutVM: WorkoutViewModel
@AppStorage("streakGoal") private var streakGoal = 5
@ObservedObject private var hk = HealthKitManager.shared
@ObservedObject private var auth = AuthManager.shared
@State private var showActivityHistory = false
// Task metrics
private var totalTasks: Int { taskVM.tasks.count }
private var doneTasks: Int { taskVM.tasks.filter { $0.isComplete }.count }
private var overdueCount: Int { taskVM.overdueTasks.count }
private var completionRate: Int {
guard totalTasks > 0 else { return 0 }
return Int(Double(doneTasks) / Double(totalTasks) * 100)
}
private var completedThisWeek: Int {
// Sum of per-day counts includes recurring occurrences.
last7.reduce(0) { $0 + $1.1 }
}
private var urgentOpen: Int {
taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .urgent }.count
}
// 7-day task activity (recurring occurrences included)
private var last7: [(Date, Int)] {
let cal = Calendar.current
return (0..<7).reversed().map { ago -> (Date, Int) in
let day = cal.date(byAdding: .day, value: -ago, to: cal.startOfDay(for: Date()))!
return (day, taskVM.completionCount(on: day))
}
}
private var maxDay: Int { max(1, last7.map { $0.1 }.max() ?? 1) }
// Workout metrics
private var scheduledDays: Int { workoutVM.schedule.count }
private let weekRow: [(Int, String)] = [(2,"M"),(3,"T"),(4,"W"),(5,"T"),(6,"F"),(7,"S"),(1,"S")]
private var todayWD: Int { Calendar.current.component(.weekday, from: Date()) }
var body: some View {
VStack(spacing: 0) {
HStack {
Spacer()
Button("Done") { dismiss() }
.font(AppFonts.sans(13, weight: .semibold))
.foregroundColor(AppColors.accent).buttonStyle(.plain)
}
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 4)
ScrollView(showsIndicators: false) {
VStack(spacing: 14) {
// Avatar
VStack(spacing: 8) {
ZStack {
Circle().fill(AppColors.accentSoft)
Text(initials(for: auth.currentUser?.displayName))
.font(AppFonts.sans(24, weight: .bold)).foregroundColor(AppColors.accent)
}
.frame(width: 68, height: 68)
.overlay(Circle().stroke(AppColors.accent.opacity(0.25), lineWidth: 2))
Text(auth.currentUser?.displayName ?? "My Account")
.font(AppFonts.sans(18, weight: .bold)).foregroundColor(AppColors.text(cs))
Text(auth.currentUser?.email ?? "kisaniCAL.")
.font(AppFonts.mono(11)).foregroundColor(AppColors.text3(cs))
}
.frame(maxWidth: .infinity).padding(.vertical, 14)
// Tasks card
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("TASKS").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
Spacer()
Button { showActivityHistory = true } label: {
Text("History")
.font(AppFonts.mono(9, weight: .bold))
.foregroundColor(AppColors.text2(cs))
}
.buttonStyle(.plain)
}
HStack(spacing: 8) {
PStatCell(value: "\(taskVM.taskStreakDays)", label: "day streak", color: taskVM.taskStreakDays > 0 ? AppColors.green : AppColors.text3(cs))
PStatCell(value: "\(completedThisWeek)", label: "this week", color: AppColors.green)
PStatCell(value: "\(overdueCount)", label: "overdue", color: overdueCount > 0 ? AppColors.accent : AppColors.text3(cs))
PStatCell(value: "\(completionRate)%", label: "done rate", color: AppColors.blue)
}
// 7-day bar chart
VStack(alignment: .leading, spacing: 6) {
Text("COMPLETED LAST 7 DAYS")
.font(AppFonts.mono(7.5, weight: .bold)).foregroundColor(AppColors.text3(cs))
HStack(alignment: .bottom, spacing: 5) {
ForEach(last7, id: \.0) { day, count in
VStack(spacing: 3) {
if count > 0 {
Text("\(count)")
.font(AppFonts.mono(7.5, weight: .bold))
.foregroundColor(AppColors.green)
}
RoundedRectangle(cornerRadius: 3)
.fill(count > 0 ? AppColors.green : AppColors.borderHi(cs))
.frame(height: count > 0 ? max(8, CGFloat(count) / CGFloat(maxDay) * 44) : 8)
Text(dayLetter(day))
.font(AppFonts.mono(7.5, weight: .bold))
.foregroundColor(Calendar.current.isDateInToday(day) ? AppColors.accent : AppColors.text3(cs))
}
.frame(maxWidth: .infinity)
}
}
.frame(height: 62)
}
// Quadrant breakdown
HStack(spacing: 8) {
QuadStat(label: "Urgent", count: taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .urgent }.count, color: AppColors.accent)
QuadStat(label: "Schedule", count: taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .schedule }.count, color: AppColors.yellow)
QuadStat(label: "Delegate", count: taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .delegate_ }.count, color: AppColors.blue)
QuadStat(label: "Eliminate",count: taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .eliminate }.count, color: AppColors.green)
}
}
.padding(14).background(AppColors.surface(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
.overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.border(cs), lineWidth: 1))
// Workouts card
VStack(alignment: .leading, spacing: 12) {
Text("WORKOUTS").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
HStack(spacing: 8) {
PStatCell(value: "\(workoutVM.streakDays)", label: streakBars(workoutVM.streakDays, goal: streakGoal), color: workoutVM.streakDays >= streakGoal ? AppColors.green : workoutVM.streakDays > 0 ? AppColors.accent : AppColors.text3(cs))
PStatCell(value: "\(workoutVM.doneSets)/\(workoutVM.totalSets)", label: "sets today", color: AppColors.blue)
PStatCell(value: "\(Int(workoutVM.progress * 100))%", label: "today", color: workoutVM.progress >= 1 ? AppColors.green : AppColors.text2(cs))
}
// Weekly schedule grid
VStack(alignment: .leading, spacing: 6) {
Text("THIS WEEK'S SCHEDULE")
.font(AppFonts.mono(7.5, weight: .bold)).foregroundColor(AppColors.text3(cs))
HStack(spacing: 5) {
ForEach(weekRow, id: \.0) { wd, letter in
let scheduled = workoutVM.schedule[wd] != nil
let isToday = wd == todayWD
let done = isToday && workoutVM.progress >= 1
VStack(spacing: 4) {
ZStack {
Circle()
.fill(scheduled
? (done ? AppColors.greenSoft : (isToday ? AppColors.accentSoft : AppColors.surface2(cs)))
: AppColors.surface2(cs))
.frame(width: 30, height: 30)
if scheduled {
if done {
Image(systemName: "checkmark")
.font(.system(size: 9, weight: .bold))
.foregroundColor(AppColors.green)
} else {
Image(systemName: "dumbbell.fill")
.font(.system(size: 8))
.foregroundColor(isToday ? AppColors.accent : AppColors.text3(cs))
}
}
}
.overlay(Circle().stroke(isToday ? AppColors.accent : Color.clear, lineWidth: 1.5))
Text(letter)
.font(AppFonts.mono(7.5, weight: .bold))
.foregroundColor(isToday ? AppColors.accent : AppColors.text3(cs))
}
.frame(maxWidth: .infinity)
}
}
}
// Programs summary
HStack {
Image(systemName: "list.bullet.clipboard")
.font(.system(size: 13))
.foregroundColor(AppColors.blue)
.frame(width: 28, height: 28)
.background(AppColors.blueSoft)
.clipShape(RoundedRectangle(cornerRadius: 7))
VStack(alignment: .leading, spacing: 1) {
Text(workoutVM.workoutTitle)
.font(AppFonts.sans(12, weight: .semibold))
.foregroundColor(AppColors.text(cs))
Text("\(workoutVM.activeSections.count) sections · \(workoutVM.totalSets) sets total")
.font(AppFonts.mono(9)).foregroundColor(AppColors.text3(cs))
}
Spacer()
Text("ACTIVE").font(AppFonts.mono(8, weight: .bold))
.foregroundColor(AppColors.accent)
.padding(.horizontal, 7).padding(.vertical, 3)
.background(AppColors.accentSoft).cornerRadius(5)
}
.padding(10)
.background(AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
}
.padding(14).background(AppColors.surface(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
.overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.border(cs), lineWidth: 1))
// Health card
if hk.authorized {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 6) {
Text("HEALTH")
.font(AppFonts.mono(9, weight: .bold))
.foregroundColor(AppColors.text3(cs))
Image(systemName: "heart.fill")
.font(.system(size: 8))
.foregroundColor(AppColors.green)
}
HStack(spacing: 8) {
PStatCell(value: "\(hk.stepsToday.formatted())", label: "steps today", color: AppColors.blue)
PStatCell(value: "\(hk.activeCaloriesToday)", label: "kcal active", color: AppColors.accent)
PStatCell(value: hk.restingHeartRate > 0 ? "\(hk.restingHeartRate)" : "",
label: "bpm resting", color: AppColors.green)
}
HStack(spacing: 10) {
Image(systemName: "figure.strengthtraining.traditional")
.font(.system(size: 13))
.foregroundColor(AppColors.green)
.frame(width: 28, height: 28)
.background(AppColors.greenSoft)
.clipShape(RoundedRectangle(cornerRadius: 7))
VStack(alignment: .leading, spacing: 1) {
Text("Workouts this week")
.font(AppFonts.sans(12, weight: .semibold))
.foregroundColor(AppColors.text(cs))
Text("From Apple Health")
.font(AppFonts.mono(9))
.foregroundColor(AppColors.text3(cs))
}
Spacer()
Text("\(hk.workoutsThisWeek)")
.font(AppFonts.mono(17, weight: .bold))
.foregroundColor(hk.workoutsThisWeek > 0 ? AppColors.green : AppColors.text3(cs))
}
.padding(10)
.background(AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
}
.padding(14).background(AppColors.surface(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
.overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.green.opacity(0.25), lineWidth: 1))
.onAppear { Task { await hk.refresh() } }
}
}
.padding(.horizontal, 14).padding(.bottom, 30)
}
}
.background(AppColors.background(cs).ignoresSafeArea())
.sheet(isPresented: $showActivityHistory) {
ActivityHistoryView()
.environmentObject(taskVM)
.environmentObject(workoutVM)
}
}
private func dayLetter(_ date: Date) -> String {
let f = DateFormatter(); f.dateFormat = "E"
return String(f.string(from: date).prefix(1))
}
}
private struct PStatCell: View {
@Environment(\.colorScheme) private var cs
let value: String; let label: String; let color: Color; var suffix: String = ""
var body: some View {
VStack(spacing: 4) {
HStack(spacing: 3) {
Text(value).font(AppFonts.mono(17, weight: .bold)).foregroundColor(color)
if !suffix.isEmpty { Text(suffix).font(.system(size: 13)) }
}
Text(label).font(AppFonts.mono(8)).foregroundColor(AppColors.text3(cs)).multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity).padding(.vertical, 12)
.background(AppColors.surface2(cs)).clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
}
}
private func initials(for name: String?) -> String {
guard let name, !name.isEmpty else { return "?" }
let parts = name.split(separator: " ")
if parts.count >= 2 {
return (String(parts[0].prefix(1)) + String(parts[1].prefix(1))).uppercased()
}
return String(name.prefix(2)).uppercased()
}
private func streakBars(_ streak: Int, goal: Int) -> String {
let filled = min(max(streak, 0), goal)
return String(repeating: "|", count: filled) + String(repeating: "·", count: goal - filled)
}
private struct QuadStat: View {
@Environment(\.colorScheme) private var cs
let label: String; let count: Int; let color: Color
var body: some View {
VStack(spacing: 3) {
Text("\(count)").font(AppFonts.mono(14, weight: .bold)).foregroundColor(count > 0 ? color : AppColors.text3(cs))
Text(label).font(AppFonts.mono(7, weight: .bold)).foregroundColor(AppColors.text3(cs)).lineLimit(1).minimumScaleFactor(0.7)
}
.frame(maxWidth: .infinity).padding(.vertical, 8)
.background(count > 0 ? color.opacity(0.08) : AppColors.surface2(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
.overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(count > 0 ? color.opacity(0.25) : AppColors.border(cs), lineWidth: 1))
}
}
// MARK: - About Sheet
private struct AboutSheet: View {
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
var body: some View {
VStack(spacing: 0) {
SheetHeader(title: "About") { dismiss() }
VStack(spacing: 16) {
Image(systemName: "calendar.badge.clock")
.font(.system(size: 48, weight: .light)).foregroundColor(AppColors.accent)
VStack(spacing: 4) {
Text("Wenza").font(AppFonts.sans(20, weight: .bold)).foregroundColor(AppColors.text(cs))
Text("Version \(Bundle.main.versionDisplay)").font(AppFonts.mono(11)).foregroundColor(AppColors.text3(cs))
}
Text("A calm, focused productivity app for tasks, calendar, and fitness.")
.font(AppFonts.sans(13)).foregroundColor(AppColors.text2(cs))
.multilineTextAlignment(.center).padding(.horizontal, 20)
}
.padding(.top, 10)
Spacer()
}
.background(AppColors.background(cs).ignoresSafeArea())
}
}