Andromida/Andromida/App/Views/Onboarding/GoalSelectionStepView.swift

153 lines
5.5 KiB
Swift

import SwiftUI
import Bedrock
/// The goal selection screen where users choose what they want to focus on.
struct GoalSelectionStepView: View {
@Binding var selectedGoals: [OnboardingGoal]
let onContinue: () -> Void
@State private var animateCards = false
private let columns = [
GridItem(.flexible(), spacing: Design.Spacing.medium),
GridItem(.flexible(), spacing: Design.Spacing.medium)
]
var body: some View {
VStack(spacing: Design.Spacing.xxLarge) {
Spacer()
.frame(height: Design.Spacing.large)
// Header
VStack(spacing: Design.Spacing.small) {
Text(String(localized: "What would you like to focus on?"))
.typography(.title2Bold)
.foregroundStyle(AppTextColors.primary)
.multilineTextAlignment(.center)
}
.padding(.horizontal, Design.Spacing.large)
// Goal cards grid
LazyVGrid(columns: columns, spacing: Design.Spacing.medium) {
ForEach(Array(OnboardingGoal.allCases.enumerated()), id: \.element.id) { index, goal in
GoalCardView(
goal: goal,
isSelected: selectedGoals.contains(goal),
onTap: {
withAnimation(.easeInOut(duration: Design.Animation.quick)) {
toggleGoalSelection(goal)
}
}
)
.opacity(animateCards ? 1 : 0)
.offset(y: animateCards ? 0 : 20)
.animation(
.easeOut(duration: 0.4).delay(Double(index) * 0.1),
value: animateCards
)
}
}
.padding(.horizontal, Design.Spacing.large)
Spacer()
// Continue button (only shown when a goal is selected)
if !selectedGoals.isEmpty {
Button(action: onContinue) {
Text(String(localized: "Continue"))
.typography(.heading)
.foregroundStyle(AppTextColors.inverse)
.frame(maxWidth: .infinity)
.frame(height: AppMetrics.Size.buttonHeight)
.background(AppAccent.primary)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
}
.accessibilityIdentifier("onboarding.goalContinue")
.padding(.horizontal, Design.Spacing.xxLarge)
.padding(.bottom, Design.Spacing.xxLarge)
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
.animation(.easeInOut(duration: Design.Animation.quick), value: !selectedGoals.isEmpty)
.onAppear {
withAnimation {
animateCards = true
}
}
}
private func toggleGoalSelection(_ goal: OnboardingGoal) {
if let index = selectedGoals.firstIndex(of: goal) {
selectedGoals.remove(at: index)
} else {
selectedGoals.append(goal)
}
}
}
/// A single goal card in the selection grid.
private struct GoalCardView: View {
let goal: OnboardingGoal
let isSelected: Bool
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
VStack(spacing: Design.Spacing.medium) {
// Icon
SymbolIcon(goal.symbolName, size: .card, color: isSelected ? AppAccent.primary : AppTextColors.secondary)
// Text
VStack(spacing: Design.Spacing.xSmall) {
Text(goal.displayName)
.typography(.heading)
.foregroundStyle(AppTextColors.primary)
Text(goal.subtitle)
.typography(.caption)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.large)
.padding(.horizontal, Design.Spacing.medium)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.fill(isSelected ? AppAccent.primary.opacity(0.15) : AppSurface.card)
)
.overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.stroke(
isSelected ? AppAccent.primary : Color.clear,
lineWidth: 2
)
)
}
.buttonStyle(.plain)
.accessibilityElement(children: .ignore)
.accessibilityAddTraits(.isButton)
.accessibilityLabel(goal.displayName)
.accessibilityHint(goal.subtitle)
.accessibilityIdentifier("onboarding.goal.\(goal.rawValue)")
}
}
#Preview {
ZStack {
LinearGradient(
colors: [AppSurface.primary, AppSurface.secondary],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
GoalSelectionStepView(
selectedGoals: .constant([]),
onContinue: {}
)
}
}