- Full-screen views: Added WarmGradientBackground() to CompleteTaskView, ContractorDetailView, DocumentDetailView, DocumentFormView, FeatureComparisonView, TaskTemplatesBrowserView, ManageUsersView, ContractorPickerView - Onboarding: Redesigned all 8 screens with organic styling including animated hero sections, gradient buttons, decorative blobs - Components: Updated ErrorView, EmptyStateView, EmptyResidencesView, EmptyTasksView, TaskSuggestionsView, StatView, SummaryStatView, CompletionCardView, DynamicTaskColumnView with organic styling - Applied consistent patterns: OrganicSpacing, naturalShadow modifier, RoundedRectangle with .continuous style, rounded font designs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
211 lines
8.6 KiB
Swift
211 lines
8.6 KiB
Swift
import SwiftUI
|
|
|
|
/// Screen 1: Welcome screen with Start Fresh / Join Existing options
|
|
struct OnboardingWelcomeView: View {
|
|
var onStartFresh: () -> Void
|
|
var onJoinExisting: () -> Void
|
|
var onLogin: () -> Void
|
|
|
|
@State private var showingLoginSheet = false
|
|
@State private var isAnimating = false
|
|
@State private var iconScale: CGFloat = 0.8
|
|
@State private var iconOpacity: Double = 0
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
WarmGradientBackground()
|
|
|
|
// Decorative blobs
|
|
GeometryReader { geo in
|
|
OrganicBlobShape(variation: 0)
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [
|
|
Color.appPrimary.opacity(0.08),
|
|
Color.appPrimary.opacity(0.02),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: geo.size.width * 0.4
|
|
)
|
|
)
|
|
.frame(width: geo.size.width * 0.7, height: geo.size.height * 0.4)
|
|
.offset(x: -geo.size.width * 0.2, y: geo.size.height * 0.1)
|
|
.blur(radius: 30)
|
|
|
|
OrganicBlobShape(variation: 1)
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [
|
|
Color.appAccent.opacity(0.06),
|
|
Color.appAccent.opacity(0.01),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: geo.size.width * 0.3
|
|
)
|
|
)
|
|
.frame(width: geo.size.width * 0.5, height: geo.size.height * 0.3)
|
|
.offset(x: geo.size.width * 0.6, y: geo.size.height * 0.65)
|
|
.blur(radius: 25)
|
|
}
|
|
|
|
VStack(spacing: 0) {
|
|
Spacer()
|
|
|
|
// Hero section
|
|
VStack(spacing: OrganicSpacing.comfortable) {
|
|
// Animated icon with glow
|
|
ZStack {
|
|
// Outer pulsing glow
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [
|
|
Color.appPrimary.opacity(0.2),
|
|
Color.appPrimary.opacity(0.05),
|
|
Color.clear
|
|
],
|
|
center: .center,
|
|
startRadius: 0,
|
|
endRadius: 100
|
|
)
|
|
)
|
|
.frame(width: 200, height: 200)
|
|
.scaleEffect(isAnimating ? 1.1 : 1.0)
|
|
.animation(
|
|
Animation.easeInOut(duration: 2.5).repeatForever(autoreverses: true),
|
|
value: isAnimating
|
|
)
|
|
|
|
// App icon
|
|
Image("icon")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 120, height: 120)
|
|
.clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
|
|
.naturalShadow(.pronounced)
|
|
.scaleEffect(iconScale)
|
|
.opacity(iconOpacity)
|
|
}
|
|
|
|
// Welcome text
|
|
VStack(spacing: 10) {
|
|
Text("Welcome to Casera")
|
|
.font(.system(size: 32, weight: .bold, design: .rounded))
|
|
.foregroundColor(Color.appTextPrimary)
|
|
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.welcomeTitle)
|
|
|
|
Text("Your home maintenance companion")
|
|
.font(.system(size: 17, weight: .medium))
|
|
.foregroundColor(Color.appTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Action buttons
|
|
VStack(spacing: 14) {
|
|
// Primary CTA - Start Fresh
|
|
Button(action: onStartFresh) {
|
|
HStack(spacing: 12) {
|
|
Image("icon")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 24, height: 24)
|
|
Text("Start Fresh")
|
|
.font(.system(size: 17, weight: .semibold))
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: 56)
|
|
.foregroundColor(Color.appTextOnPrimary)
|
|
.background(
|
|
LinearGradient(
|
|
colors: [Color.appPrimary, Color.appPrimary.opacity(0.85)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
|
.naturalShadow(.medium)
|
|
}
|
|
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.startFreshButton)
|
|
|
|
// Secondary CTA - Join Existing
|
|
Button(action: onJoinExisting) {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: "person.2.fill")
|
|
.font(.system(size: 18, weight: .medium))
|
|
Text("I have a code to join")
|
|
.font(.system(size: 17, weight: .medium))
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: 56)
|
|
.foregroundColor(Color.appPrimary)
|
|
.background(Color.appPrimary.opacity(0.1))
|
|
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
.stroke(Color.appPrimary.opacity(0.2), lineWidth: 1)
|
|
)
|
|
}
|
|
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.joinExistingButton)
|
|
|
|
// Returning user login
|
|
Button(action: {
|
|
showingLoginSheet = true
|
|
}) {
|
|
Text("Already have an account? Log in")
|
|
.font(.system(size: 15, weight: .medium))
|
|
.foregroundColor(Color.appTextSecondary)
|
|
}
|
|
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.loginButton)
|
|
.padding(.top, 8)
|
|
}
|
|
.padding(.horizontal, OrganicSpacing.comfortable)
|
|
.padding(.bottom, OrganicSpacing.airy)
|
|
|
|
// Floating leaves decoration
|
|
HStack(spacing: 50) {
|
|
FloatingLeaf(delay: 0, size: 16, color: Color.appPrimary)
|
|
FloatingLeaf(delay: 0.5, size: 12, color: Color.appAccent)
|
|
FloatingLeaf(delay: 1.0, size: 18, color: Color.appPrimary)
|
|
}
|
|
.opacity(0.5)
|
|
.padding(.bottom, 20)
|
|
}
|
|
}
|
|
.sheet(isPresented: $showingLoginSheet) {
|
|
LoginView(onLoginSuccess: {
|
|
showingLoginSheet = false
|
|
onLogin()
|
|
})
|
|
}
|
|
.onAppear {
|
|
isAnimating = true
|
|
withAnimation(.spring(response: 0.8, dampingFraction: 0.7)) {
|
|
iconScale = 1.0
|
|
iconOpacity = 1.0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Content-only version (no navigation bar)
|
|
|
|
/// Content-only version for use in coordinator
|
|
typealias OnboardingWelcomeContent = OnboardingWelcomeView
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
OnboardingWelcomeView(
|
|
onStartFresh: {},
|
|
onJoinExisting: {},
|
|
onLogin: {}
|
|
)
|
|
}
|