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.
This commit is contained in:
kutesir
2026-05-28 00:29:06 +03:00
parent 234ef622f4
commit 07bc2eee1c
2 changed files with 159 additions and 1 deletions

View File

@@ -3,10 +3,18 @@ import SwiftUI
@main @main
struct KisaniCalApp: App { struct KisaniCalApp: App {
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
@State private var splashDone = false
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() ZStack {
ContentView()
if !splashDone {
SplashView { splashDone = true }
.transition(.opacity)
}
}
.animation(.easeIn(duration: 0.2), value: splashDone)
} }
} }
} }

View File

@@ -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..<rowCount, id: \.self) { row in
HStack(spacing: 0) {
ForEach(0..<7, id: \.self) { col in
let day = days[row * 7 + col]
DayCell(day: day, isToday: day == today, cs: cs)
}
}
}
}
.padding(.horizontal, 18)
.padding(.vertical, 16)
.frame(width: 244)
.background(AppColors.surface(cs))
.clipShape(RoundedRectangle(cornerRadius: 20))
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(AppColors.border(cs), lineWidth: 1)
)
.shadow(color: Color.black.opacity(cs == .dark ? 0.3 : 0.06), radius: 24, y: 8)
}
}
private struct DayCell: View {
let day: Int?
let isToday: Bool
let cs: ColorScheme
var body: some View {
ZStack {
if isToday {
Circle()
.fill(AppColors.accent)
.frame(width: 26, height: 26)
}
if let d = day {
Text("\(d)")
.font(AppFonts.mono(isToday ? 10 : 9.5, weight: isToday ? .bold : .regular))
.foregroundColor(isToday ? .white : AppColors.text(cs).opacity(0.72))
}
}
.frame(maxWidth: .infinity)
.frame(height: 28)
}
}