Initial commit: KisaniCal iOS app
Task management, Eisenhower matrix, workout logging with persistence, and calendar with 6 view modes (Month, List, Year, Week, 3 Day, Day). Custom floating tab bar, dynamic calendar icon, and workout data seeded.
This commit is contained in:
261
KisaniCal/Views/AddExerciseSheet.swift
Normal file
261
KisaniCal/Views/AddExerciseSheet.swift
Normal file
@@ -0,0 +1,261 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Exercise Picker Sheet
|
||||
struct ExercisePickerSheet: View {
|
||||
@EnvironmentObject var vm: WorkoutViewModel
|
||||
@Environment(\.colorScheme) var cs
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
let sectionId: UUID
|
||||
var programId: UUID? = nil // nil = active program
|
||||
|
||||
@State private var selectedMuscle: ExerciseMuscle = .chest
|
||||
@State private var searchText = ""
|
||||
@State private var customName = ""
|
||||
@State private var setsText = "3"
|
||||
@State private var repsText = "10"
|
||||
@State private var showCustom = false
|
||||
@FocusState private var customFocused: Bool
|
||||
|
||||
private func addExercise(name: String) {
|
||||
let sets = Int(setsText) ?? 3
|
||||
let reps = Int(repsText) ?? 10
|
||||
if let pid = programId {
|
||||
vm.addExercise(programId: pid, sectionId: sectionId, name: name, setsCount: sets, repsCount: reps)
|
||||
} else {
|
||||
vm.addExercise(to: sectionId, name: name, setsCount: sets, repsCount: reps)
|
||||
}
|
||||
}
|
||||
|
||||
private var filtered: [ExerciseTemplate] {
|
||||
let byMuscle = exerciseLibrary.filter { $0.muscle == selectedMuscle }
|
||||
guard !searchText.isEmpty else { return byMuscle }
|
||||
return byMuscle.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// ── Header ──
|
||||
HStack {
|
||||
Text("Add Exercise")
|
||||
.font(AppFonts.sans(17, weight: .bold))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
Spacer()
|
||||
Button("Cancel") { dismiss() }
|
||||
.font(AppFonts.sans(13))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 12)
|
||||
|
||||
Divider().background(AppColors.border(cs))
|
||||
|
||||
// ── Search ──
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
TextField("Search exercises…", text: $searchText)
|
||||
.font(AppFonts.sans(13))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
if !searchText.isEmpty {
|
||||
Button { searchText = "" } label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.background(AppColors.surface2(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
|
||||
.overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(AppColors.border(cs), lineWidth: 1))
|
||||
.padding(.horizontal, 16).padding(.top, 12).padding(.bottom, 8)
|
||||
|
||||
// ── Category Tabs ──
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(ExerciseMuscle.allCases) { muscle in
|
||||
let isSel = selectedMuscle == muscle
|
||||
Button { withAnimation(KisaniSpring.micro) { selectedMuscle = muscle } } label: {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: muscle.sfSymbol).font(.system(size: 10))
|
||||
Text(muscle.rawValue).font(AppFonts.mono(9.5, weight: .bold))
|
||||
}
|
||||
.foregroundColor(isSel ? AppColors.accent : AppColors.text3(cs))
|
||||
.padding(.horizontal, 11).padding(.vertical, 6)
|
||||
.background(isSel ? AppColors.accentSoft : AppColors.surface2(cs))
|
||||
.clipShape(Capsule())
|
||||
.overlay(Capsule().stroke(isSel ? AppColors.accent : AppColors.border(cs), lineWidth: 1))
|
||||
}
|
||||
.buttonStyle(PressButtonStyle(scale: 0.94))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
|
||||
// ── Exercise List ──
|
||||
ScrollView(showsIndicators: false) {
|
||||
LazyVStack(spacing: 0) {
|
||||
// Custom exercise entry
|
||||
if showCustom {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("CUSTOM EXERCISE").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs))
|
||||
TextField("Exercise name", text: $customName)
|
||||
.font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs))
|
||||
.focused($customFocused).padding(11)
|
||||
.background(AppColors.surface2(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.small))
|
||||
.overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(customFocused ? AppColors.accent : AppColors.border(cs), lineWidth: 1))
|
||||
SetRepsRow(setsText: $setsText, repsText: $repsText)
|
||||
Button {
|
||||
let n = customName.trimmingCharacters(in: .whitespaces)
|
||||
guard !n.isEmpty else { return }
|
||||
addExercise(name: n)
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("Add Custom Exercise")
|
||||
.font(AppFonts.sans(13, weight: .bold)).foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity).padding(11)
|
||||
.background(!customName.trimmingCharacters(in: .whitespaces).isEmpty ? AppColors.accent : AppColors.borderHi(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(customName.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||
}
|
||||
.padding(14)
|
||||
.background(AppColors.surface2(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.medium))
|
||||
.overlay(RoundedRectangle(cornerRadius: AppRadius.medium).stroke(AppColors.accent.opacity(0.3), lineWidth: 1))
|
||||
.padding(.horizontal, 16).padding(.bottom, 8)
|
||||
}
|
||||
|
||||
// Library exercises
|
||||
ForEach(filtered) { template in
|
||||
ExerciseTemplateRow(template: template) {
|
||||
addExercise(name: template.name)
|
||||
dismiss()
|
||||
}
|
||||
Divider().background(AppColors.border(cs)).padding(.leading, 60)
|
||||
}
|
||||
|
||||
if filtered.isEmpty && !showCustom {
|
||||
VStack(spacing: 6) {
|
||||
Text("No exercises found")
|
||||
.font(AppFonts.sans(13, weight: .medium))
|
||||
.foregroundColor(AppColors.text2(cs))
|
||||
Text("Try a different category or add a custom one")
|
||||
.font(AppFonts.sans(11))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(maxWidth: .infinity).padding(.vertical, 30)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
// ── Footer: Default sets/reps + custom toggle ──
|
||||
Divider().background(AppColors.border(cs))
|
||||
VStack(spacing: 10) {
|
||||
SetRepsRow(setsText: $setsText, repsText: $repsText)
|
||||
Button {
|
||||
withAnimation(KisaniSpring.snappy) {
|
||||
showCustom.toggle()
|
||||
if showCustom { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { customFocused = true } }
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: showCustom ? "xmark.circle" : "plus.circle")
|
||||
.font(.system(size: 13))
|
||||
Text(showCustom ? "Cancel Custom" : "Add Custom Exercise")
|
||||
.font(AppFonts.sans(12, weight: .semibold))
|
||||
}
|
||||
.foregroundColor(AppColors.accent)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 16).padding(.vertical, 12)
|
||||
}
|
||||
.background(AppColors.background(cs).ignoresSafeArea())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Exercise Template Row
|
||||
private struct ExerciseTemplateRow: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
let template: ExerciseTemplate
|
||||
let onAdd: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: onAdd) {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: template.sfSymbol)
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.foregroundColor(AppColors.accent)
|
||||
.frame(width: 36, height: 36)
|
||||
.background(AppColors.accentSoft)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 9))
|
||||
|
||||
Text(template.name)
|
||||
.font(AppFonts.sans(13, weight: .medium))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(AppColors.accent.opacity(0.7))
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sets / Reps Row (shared)
|
||||
private struct SetRepsRow: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
@Binding var setsText: String
|
||||
@Binding var repsText: String
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
HStack(spacing: 6) {
|
||||
Text("Sets").font(AppFonts.mono(9.5, weight: .bold)).foregroundColor(AppColors.text3(cs))
|
||||
TextField("3", text: $setsText)
|
||||
.font(AppFonts.mono(12, weight: .semibold)).foregroundColor(AppColors.text(cs))
|
||||
.keyboardType(.numberPad).multilineTextAlignment(.center)
|
||||
.frame(width: 36).padding(.vertical, 5).padding(.horizontal, 4)
|
||||
.background(AppColors.surface2(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(AppColors.border(cs), lineWidth: 1))
|
||||
}
|
||||
.padding(8)
|
||||
.background(AppColors.surface(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(AppColors.border(cs), lineWidth: 1))
|
||||
|
||||
HStack(spacing: 6) {
|
||||
Text("Reps").font(AppFonts.mono(9.5, weight: .bold)).foregroundColor(AppColors.text3(cs))
|
||||
TextField("10", text: $repsText)
|
||||
.font(AppFonts.mono(12, weight: .semibold)).foregroundColor(AppColors.text(cs))
|
||||
.keyboardType(.numberPad).multilineTextAlignment(.center)
|
||||
.frame(width: 36).padding(.vertical, 5).padding(.horizontal, 4)
|
||||
.background(AppColors.surface2(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(AppColors.border(cs), lineWidth: 1))
|
||||
}
|
||||
.padding(8)
|
||||
.background(AppColors.surface(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(AppColors.border(cs), lineWidth: 1))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
1037
KisaniCal/Views/CalendarView.swift
Normal file
1037
KisaniCal/Views/CalendarView.swift
Normal file
File diff suppressed because it is too large
Load Diff
192
KisaniCal/Views/MatrixView.swift
Normal file
192
KisaniCal/Views/MatrixView.swift
Normal file
@@ -0,0 +1,192 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MatrixView: View {
|
||||
@Environment(\.colorScheme) var cs
|
||||
@EnvironmentObject var taskVM: TaskViewModel
|
||||
@State private var showAddTask = false
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
AppColors.background(cs).ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// ── Header ──
|
||||
HStack {
|
||||
Text("Eisenhower Matrix")
|
||||
.font(AppFonts.sans(15, weight: .semibold))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
Spacer()
|
||||
IButton(icon: "ellipsis")
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
// ── 2×2 Grid — fixed equal quadrants ──
|
||||
VStack(spacing: 7) {
|
||||
HStack(spacing: 7) {
|
||||
QuadrantCard(
|
||||
roman: "I", label: "Urgent & Important",
|
||||
badgeBg: AppColors.accentSoft, badgeColor: AppColors.accent,
|
||||
tasks: taskVM.tasks(for: .urgent)
|
||||
) { taskVM.toggle($0) }
|
||||
|
||||
QuadrantCard(
|
||||
roman: "II", label: "Not Urgent & Important",
|
||||
badgeBg: AppColors.yellowSoft, badgeColor: AppColors.yellow,
|
||||
tasks: taskVM.tasks(for: .schedule)
|
||||
) { taskVM.toggle($0) }
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
|
||||
HStack(spacing: 7) {
|
||||
QuadrantCard(
|
||||
roman: "III", label: "Urgent & Unimportant",
|
||||
badgeBg: AppColors.blueSoft, badgeColor: AppColors.blue,
|
||||
tasks: taskVM.tasks(for: .delegate_)
|
||||
) { taskVM.toggle($0) }
|
||||
|
||||
QuadrantCard(
|
||||
roman: "IV", label: "Not Urgent & Unimportant",
|
||||
badgeBg: AppColors.greenSoft, badgeColor: AppColors.green,
|
||||
tasks: taskVM.tasks(for: .eliminate)
|
||||
) { taskVM.toggle($0) }
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
|
||||
FAB { showAddTask = true }
|
||||
.padding(.trailing, 20)
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
.sheet(isPresented: $showAddTask) {
|
||||
AddTaskSheet()
|
||||
.environmentObject(taskVM)
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Quadrant Card
|
||||
struct QuadrantCard: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
let roman: String
|
||||
let label: String
|
||||
let badgeBg: Color
|
||||
let badgeColor: Color
|
||||
let tasks: [TaskItem]
|
||||
let onToggle: (TaskItem) -> Void
|
||||
|
||||
private let shortFmt: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "d MMM"
|
||||
return f
|
||||
}()
|
||||
|
||||
private let shortFmtYear: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "d/MM/yyyy"
|
||||
return f
|
||||
}()
|
||||
|
||||
private func dateLabel(_ date: Date) -> String {
|
||||
let cal = Calendar.current
|
||||
if cal.isDateInThisYear(date) {
|
||||
return shortFmt.string(from: date)
|
||||
}
|
||||
return shortFmtYear.string(from: date)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// ── Header badge ──
|
||||
HStack(spacing: 4) {
|
||||
Text(roman)
|
||||
.font(AppFonts.mono(7.5, weight: .heavy))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 13, height: 13)
|
||||
.background(badgeColor)
|
||||
.clipShape(Circle())
|
||||
Text(label.uppercased())
|
||||
.font(AppFonts.mono(8, weight: .heavy))
|
||||
.foregroundColor(badgeColor)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.65)
|
||||
}
|
||||
.padding(.horizontal, 7)
|
||||
.padding(.vertical, 3)
|
||||
.background(badgeBg)
|
||||
.cornerRadius(20)
|
||||
.padding(.bottom, 7)
|
||||
|
||||
// ── Scrollable task list ──
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(tasks) { task in
|
||||
HStack(alignment: .top, spacing: 6) {
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.stroke(
|
||||
task.isComplete ? AppColors.border(cs) : badgeColor,
|
||||
lineWidth: 1.5
|
||||
)
|
||||
.background(
|
||||
task.isComplete
|
||||
? RoundedRectangle(cornerRadius: 3)
|
||||
.fill(AppColors.border(cs).opacity(0.5))
|
||||
: nil
|
||||
)
|
||||
.overlay(
|
||||
task.isComplete
|
||||
? Image(systemName: "checkmark")
|
||||
.font(.system(size: 6.5, weight: .bold))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
: nil
|
||||
)
|
||||
.frame(width: 13, height: 13)
|
||||
.padding(.top, 1.5)
|
||||
.contentShape(Rectangle().size(CGSize(width: 28, height: 28)))
|
||||
.onTapGesture {
|
||||
withAnimation(KisaniSpring.bounce) { onToggle(task) }
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text(task.title)
|
||||
.font(AppFonts.sans(10.5))
|
||||
.foregroundColor(task.isComplete ? AppColors.text3(cs) : AppColors.text(cs))
|
||||
.strikethrough(task.isComplete, color: AppColors.text3(cs))
|
||||
.lineLimit(2)
|
||||
if let d = task.dueDate {
|
||||
Text(dateLabel(d))
|
||||
.font(AppFonts.mono(8.5))
|
||||
.foregroundColor(task.isComplete ? AppColors.text3(cs) : badgeColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
.opacity(task.isComplete ? 0.38 : 1)
|
||||
.padding(.vertical, 3.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.background(AppColors.surface(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.medium))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: AppRadius.medium)
|
||||
.stroke(AppColors.border(cs), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Calendar {
|
||||
func isDateInThisYear(_ date: Date) -> Bool {
|
||||
component(.year, from: date) == component(.year, from: Date())
|
||||
}
|
||||
}
|
||||
223
KisaniCal/Views/OnboardingView.swift
Normal file
223
KisaniCal/Views/OnboardingView.swift
Normal file
@@ -0,0 +1,223 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OnboardingView: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
@AppStorage("bodyWeightKg") private var bodyWeightKg: Double = 0
|
||||
@AppStorage("heightCm") private var heightCm: Double = 0
|
||||
@AppStorage("weightUnit") private var weightUnit: Int = 0 // 0 = kg, 1 = lbs
|
||||
|
||||
@State private var weightStr = ""
|
||||
@State private var heightStr = ""
|
||||
@FocusState private var focusedField: Field?
|
||||
|
||||
fileprivate enum Field { case weight, height }
|
||||
|
||||
private var canContinue: Bool {
|
||||
(Double(weightStr) ?? 0) > 0 && (Double(heightStr) ?? 0) > 0
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
AppColors.background(cs).ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
// ── Brand mark ──
|
||||
VStack(spacing: 14) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 22)
|
||||
.fill(AppColors.accentSoft)
|
||||
.frame(width: 72, height: 72)
|
||||
Image(systemName: "dumbbell.fill")
|
||||
.font(.system(size: 32, weight: .semibold))
|
||||
.foregroundColor(AppColors.accent)
|
||||
}
|
||||
VStack(spacing: 6) {
|
||||
Text("Welcome to Kisani")
|
||||
.font(AppFonts.sans(26, weight: .bold))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
Text("Personalizes your warm-up and body stats")
|
||||
.font(AppFonts.sans(14))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 40)
|
||||
|
||||
// ── Form card ──
|
||||
VStack(spacing: 0) {
|
||||
// Unit toggle
|
||||
HStack(spacing: 0) {
|
||||
ForEach([("kg", 0), ("lbs", 1)], id: \.1) { label, idx in
|
||||
Button {
|
||||
withAnimation(KisaniSpring.micro) {
|
||||
// Convert existing weight string when toggling
|
||||
if let val = Double(weightStr) {
|
||||
let kg = weightUnit == 1 ? val / 2.20462 : val
|
||||
weightUnit = idx
|
||||
let display = weightUnit == 1 ? kg * 2.20462 : kg
|
||||
weightStr = display.truncatingRemainder(dividingBy: 1) == 0
|
||||
? "\(Int(display))"
|
||||
: String(format: "%.1f", display)
|
||||
} else {
|
||||
weightUnit = idx
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(label)
|
||||
.font(AppFonts.mono(12, weight: .bold))
|
||||
.foregroundColor(weightUnit == idx ? AppColors.accent : AppColors.text3(cs))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
.background(weightUnit == idx ? AppColors.accentSoft : Color.clear)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 7))
|
||||
}
|
||||
.buttonStyle(PressButtonStyle(scale: 0.96))
|
||||
}
|
||||
}
|
||||
.padding(4)
|
||||
.background(AppColors.surface2(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(AppColors.border(cs), lineWidth: 1))
|
||||
.padding(.horizontal, 28)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
// Fields
|
||||
VStack(spacing: 0) {
|
||||
OnboardingField(
|
||||
icon: "scalemass.fill",
|
||||
label: "Body Weight",
|
||||
unit: weightUnit == 0 ? "kg" : "lbs",
|
||||
placeholder: weightUnit == 0 ? "e.g. 80" : "e.g. 176",
|
||||
text: $weightStr,
|
||||
keyboard: .decimalPad,
|
||||
focused: $focusedField,
|
||||
tag: .weight
|
||||
)
|
||||
Divider().background(AppColors.border(cs)).padding(.leading, 56)
|
||||
OnboardingField(
|
||||
icon: "ruler.fill",
|
||||
label: "Height",
|
||||
unit: "cm",
|
||||
placeholder: "e.g. 175",
|
||||
text: $heightStr,
|
||||
keyboard: .numberPad,
|
||||
focused: $focusedField,
|
||||
tag: .height
|
||||
)
|
||||
}
|
||||
.cardStyle()
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
// ── Hint ──
|
||||
Text("Used for bodyweight exercises and warm-up loads.\nEdit anytime in Settings → Workout Settings.")
|
||||
.font(AppFonts.sans(11))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.top, 14)
|
||||
|
||||
Spacer()
|
||||
|
||||
// ── CTA ──
|
||||
Button { commit() } label: {
|
||||
HStack(spacing: 8) {
|
||||
Text("Get Started")
|
||||
.font(AppFonts.sans(15, weight: .bold))
|
||||
Image(systemName: "arrow.right")
|
||||
.font(.system(size: 13, weight: .bold))
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(canContinue ? AppColors.accent : AppColors.borderHi(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
|
||||
}
|
||||
.buttonStyle(PressButtonStyle())
|
||||
.disabled(!canContinue)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 36)
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Spacer()
|
||||
Button("Done") { focusedField = nil }
|
||||
.font(AppFonts.sans(13, weight: .semibold))
|
||||
.foregroundColor(AppColors.accent)
|
||||
}
|
||||
}
|
||||
.onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { focusedField = .weight } }
|
||||
}
|
||||
|
||||
private func commit() {
|
||||
guard canContinue else { return }
|
||||
let rawWeight = Double(weightStr) ?? 0
|
||||
// Always store in kg internally
|
||||
bodyWeightKg = weightUnit == 1 ? rawWeight / 2.20462 : rawWeight
|
||||
heightCm = Double(heightStr) ?? 0
|
||||
withAnimation(KisaniSpring.exit) { isPresented = false }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Field Row
|
||||
private struct OnboardingField: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
let icon: String
|
||||
let label: String
|
||||
let unit: String
|
||||
let placeholder: String
|
||||
@Binding var text: String
|
||||
let keyboard: UIKeyboardType
|
||||
var focused: FocusState<OnboardingView.Field?>.Binding
|
||||
let tag: OnboardingView.Field
|
||||
|
||||
var isFocused: Bool { focused.wrappedValue == tag }
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 15))
|
||||
.foregroundColor(isFocused ? AppColors.accent : AppColors.text3(cs))
|
||||
.frame(width: 32, height: 32)
|
||||
.background(isFocused ? AppColors.accentSoft : AppColors.surface3(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.animation(KisaniSpring.micro, value: isFocused)
|
||||
|
||||
Text(label)
|
||||
.font(AppFonts.sans(13))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 5) {
|
||||
TextField(placeholder, text: $text)
|
||||
.font(AppFonts.mono(14, weight: .semibold))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
.multilineTextAlignment(.trailing)
|
||||
.keyboardType(keyboard)
|
||||
.focused(focused, equals: tag)
|
||||
.frame(width: 70)
|
||||
.onChange(of: text) { val in
|
||||
let clean: String
|
||||
if keyboard == .decimalPad {
|
||||
clean = String(val.filter { $0.isNumber || $0 == "." }.prefix(6))
|
||||
} else {
|
||||
clean = String(val.filter { $0.isNumber }.prefix(3))
|
||||
}
|
||||
if clean != val { text = clean }
|
||||
}
|
||||
|
||||
Text(unit)
|
||||
.font(AppFonts.mono(10, weight: .bold))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
.frame(width: 26, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
}
|
||||
765
KisaniCal/Views/SettingsView.swift
Normal file
765
KisaniCal/Views/SettingsView.swift
Normal file
@@ -0,0 +1,765 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.colorScheme) var cs
|
||||
@EnvironmentObject var workoutVM: WorkoutViewModel
|
||||
|
||||
// Persisted settings
|
||||
@AppStorage("appearanceMode") private var appearanceMode = 0
|
||||
@AppStorage("soundsOn") private var soundsOn = true
|
||||
@AppStorage("healthOn") private var healthOn = false
|
||||
@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
|
||||
|
||||
// Sheet presentation
|
||||
@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 showBackup = false
|
||||
@State private var showWorkoutSettings = 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.restTimerSeconds)s" }
|
||||
private var weightLabel: String { weightUnit == 0 ? "kg" : "lbs" }
|
||||
private var dateLabel: String { ["MMM d", "MM/DD", "DD/MM"][dateFormat] }
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
AppColors.background(cs).ignoresSafeArea()
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
|
||||
Text("Settings")
|
||||
.font(AppFonts.sans(15, weight: .semibold))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
.padding(.top, 10).padding(.bottom, 4)
|
||||
|
||||
// ── Profile ──
|
||||
HStack(spacing: 12) {
|
||||
ZStack {
|
||||
Circle().fill(AppColors.accentSoft)
|
||||
Text("RK")
|
||||
.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("Robin Kutesa")
|
||||
.font(AppFonts.sans(14, weight: .semibold))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
HStack(spacing: 5) {
|
||||
AppBadge(
|
||||
text: workoutVM.streakDays > 0 ? "\(workoutVM.streakDays)d streak 🔥" : "No streak yet",
|
||||
bg: AppColors.accentSoft, fg: AppColors.accent
|
||||
)
|
||||
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()
|
||||
.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)
|
||||
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: "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 }
|
||||
SttToggleRow(icon: "heart", bg: AppColors.greenSoft, fg: AppColors.green, label: "Health Integration", isOn: $healthOn, isLast: true)
|
||||
}
|
||||
|
||||
// ── 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", value: "𝕏 👾 📸") { showFollow = true }
|
||||
SttNavRow(icon: "info.circle", bg: AppColors.surface3(cs), label: "About", value: "v1.0.0", isLast: true) { showAbout = true }
|
||||
}
|
||||
|
||||
Spacer().frame(height: 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
.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: $showBackup) { BackupSheet(iCloudSync: $iCloudSync).presentationDetents([.height(280)]).presentationDragIndicator(.visible) }
|
||||
.sheet(isPresented: $showWorkoutSettings) { WorkoutSettingsSheet(unit: $weightUnit, sets: $defaultSets, reps: $defaultReps).presentationDetents([.large]).presentationDragIndicator(.visible) }
|
||||
.sheet(isPresented: $showRestTimer) { RestTimerSheet(seconds: $workoutVM.restTimerSeconds).presentationDetents([.height(340)]).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) }
|
||||
}
|
||||
}
|
||||
|
||||
// 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 Kisani Cal 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 Kisani 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 {
|
||||
@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
|
||||
@Environment(\.dismiss) var dismiss; @Environment(\.colorScheme) var cs
|
||||
|
||||
@State private var bwStr = ""
|
||||
@State private var htStr = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
SheetHeader(title: "Workout Settings") { dismiss() }
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
|
||||
// ── 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))" : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@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() }
|
||||
Text("\(seconds)s").font(AppFonts.mono(52, weight: .bold)).foregroundColor(AppColors.accent).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)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)
|
||||
}
|
||||
}
|
||||
.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=Kisani%20Cal%20Feedback") { openURL(url) }
|
||||
}
|
||||
HelpRow(icon: "questionmark.circle", label: "FAQ", sub: "Frequently asked questions") {}
|
||||
HelpRow(icon: "book", label: "User Guide", sub: "Learn how to use Kisani Cal") {}
|
||||
}
|
||||
.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: - 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("Kisani Cal").font(AppFonts.sans(20, weight: .bold)).foregroundColor(AppColors.text(cs))
|
||||
Text("Version 1.0.0").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())
|
||||
}
|
||||
}
|
||||
1027
KisaniCal/Views/TodayView.swift
Normal file
1027
KisaniCal/Views/TodayView.swift
Normal file
File diff suppressed because it is too large
Load Diff
1252
KisaniCal/Views/WorkoutView.swift
Normal file
1252
KisaniCal/Views/WorkoutView.swift
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user