diff --git a/KisaniCal/KisaniCalApp.swift b/KisaniCal/KisaniCalApp.swift index 7ada3d4..ee75e6e 100644 --- a/KisaniCal/KisaniCalApp.swift +++ b/KisaniCal/KisaniCalApp.swift @@ -3,10 +3,18 @@ import SwiftUI @main struct KisaniCalApp: App { @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate + @State private var splashDone = false var body: some Scene { WindowGroup { - ContentView() + ZStack { + ContentView() + if !splashDone { + SplashView { splashDone = true } + .transition(.opacity) + } + } + .animation(.easeIn(duration: 0.2), value: splashDone) } } } diff --git a/KisaniCal/Views/SplashView.swift b/KisaniCal/Views/SplashView.swift new file mode 100644 index 0000000..ce6b0e1 --- /dev/null +++ b/KisaniCal/Views/SplashView.swift @@ -0,0 +1,150 @@ +import SwiftUI + +// MARK: - Splash View + +struct SplashView: View { + @Environment(\.colorScheme) private var cs + var onDismiss: () -> Void + + @State private var calendarScale: CGFloat = 0.88 + @State private var calendarOpacity: Double = 0 + @State private var wordmarkOpacity: Double = 0 + @State private var splashOpacity: Double = 1 + + var body: some View { + ZStack { + AppColors.background(cs).ignoresSafeArea() + + VStack(spacing: 22) { + SplashCalendarCard() + .scaleEffect(calendarScale) + .opacity(calendarOpacity) + + // Wordmark: kisaniCAL. + VStack(spacing: 1) { + Text("kisani") + .font(AppFonts.mono(11, weight: .regular)) + .foregroundColor(AppColors.text2(cs)) + .kerning(4) + + HStack(alignment: .firstTextBaseline, spacing: 0) { + Text("CAL") + .font(AppFonts.mono(38, weight: .bold)) + .foregroundColor(AppColors.text(cs)) + Text(".") + .font(AppFonts.mono(38, weight: .bold)) + .foregroundColor(AppColors.accent) + } + } + .opacity(wordmarkOpacity) + } + } + .opacity(splashOpacity) + .onAppear { + withAnimation(.spring(response: 0.55, dampingFraction: 0.82).delay(0.05)) { + calendarScale = 1 + calendarOpacity = 1 + } + withAnimation(.easeOut(duration: 0.35).delay(0.28)) { + wordmarkOpacity = 1 + } + withAnimation(.easeIn(duration: 0.32).delay(1.65)) { + splashOpacity = 0 + } + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + onDismiss() + } + } + } +} + +// MARK: - Calendar Card + +private struct SplashCalendarCard: View { + @Environment(\.colorScheme) private var cs + + private let cal = Calendar.current + private let today = Calendar.current.component(.day, from: Date()) + private let monthName: String = { + let fmt = DateFormatter(); fmt.dateFormat = "MMMM" + return fmt.string(from: Date()) + }() + + // Flat day array: nil = empty cell, Int = day number + private var days: [Int?] { + let now = Date() + let range = cal.range(of: .day, in: .month, for: now)!.count + let first = cal.date(from: cal.dateComponents([.year, .month], from: now))! + let lead = cal.component(.weekday, from: first) - 1 + var grid: [Int?] = Array(repeating: nil, count: lead) + grid += (1...range).map { Optional($0) } + while grid.count % 7 != 0 { grid.append(nil) } + return grid + } + + var body: some View { + VStack(spacing: 0) { + // Month label + Text(monthName.uppercased()) + .font(AppFonts.mono(9, weight: .bold)) + .foregroundColor(AppColors.text3(cs)) + .kerning(2) + .padding(.bottom, 10) + + // Day-of-week header + HStack(spacing: 0) { + ForEach(Array(["S","M","T","W","T","F","S"].enumerated()), id: \.offset) { _, d in + Text(d) + .font(AppFonts.mono(7.5, weight: .bold)) + .foregroundColor(AppColors.text3(cs)) + .frame(maxWidth: .infinity) + } + } + .padding(.bottom, 6) + + // Day grid + let rowCount = days.count / 7 + ForEach(0..