153 lines
5.5 KiB
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: {}
|
|
)
|
|
}
|
|
}
|