From 07bc2eee1cae52a3c7c6156c7e422dc5cb00d012 Mon Sep 17 00:00:00 2001 From: kutesir Date: Thu, 28 May 2026 00:29:06 +0300 Subject: [PATCH] feat: add launch splash screen with calendar and kisaniCAL. wordmark Calendar card animates in with spring, wordmark fades in 280ms later. Splash fades out at 1.65s and is removed from the view tree at 2s. --- KisaniCal/KisaniCalApp.swift | 10 ++- KisaniCal/Views/SplashView.swift | 150 +++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 KisaniCal/Views/SplashView.swift 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..