162 lines
6.2 KiB
Swift
162 lines
6.2 KiB
Swift
import SwiftUI
|
|
import Bedrock
|
|
|
|
/// The time selection screen where users choose when they want to build habits.
|
|
struct TimeSelectionStepView: View {
|
|
@Binding var selectedTimes: Set<OnboardingTimePreference>
|
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
let onContinue: () -> Void
|
|
|
|
@State private var animateCards = false
|
|
|
|
var body: some View {
|
|
VStack(spacing: Design.Spacing.xxLarge) {
|
|
Spacer()
|
|
.frame(height: Design.Spacing.large)
|
|
|
|
// Header
|
|
VStack(spacing: Design.Spacing.small) {
|
|
Text(String(localized: "When do you want to build habits?"))
|
|
.typography(.title2Bold)
|
|
.foregroundStyle(AppTextColors.primary)
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text(String(localized: "Select all that apply"))
|
|
.typography(.subheading)
|
|
.foregroundStyle(AppTextColors.secondary)
|
|
}
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
|
|
// Time preference cards
|
|
ScrollView(showsIndicators: false) {
|
|
VStack(spacing: Design.Spacing.medium) {
|
|
ForEach(Array(OnboardingTimePreference.allCases.enumerated()), id: \.element.id) { index, time in
|
|
TimeCardView(
|
|
time: time,
|
|
isSelected: selectedTimes.contains(time),
|
|
onTap: {
|
|
toggleSelection(time)
|
|
}
|
|
)
|
|
.opacity(animateCards ? 1 : 0)
|
|
.offset(y: animateCards ? 0 : 20)
|
|
.optionalAnimation(
|
|
.easeOut(duration: 0.4).delay(Double(index) * 0.1),
|
|
value: animateCards,
|
|
reduceMotion: reduceMotion
|
|
)
|
|
}
|
|
}
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
.padding(.bottom, Design.Spacing.large)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Continue button (only shown when at least one time is selected)
|
|
if !selectedTimes.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.timeContinue")
|
|
.padding(.horizontal, Design.Spacing.xxLarge)
|
|
.padding(.bottom, Design.Spacing.xxLarge)
|
|
.transition(reduceMotion ? .opacity : .move(edge: .bottom).combined(with: .opacity))
|
|
}
|
|
}
|
|
.optionalAnimation(.easeInOut(duration: Design.Animation.quick), value: !selectedTimes.isEmpty, reduceMotion: reduceMotion)
|
|
.accessibilityIdentifier("onboarding.timeSelection")
|
|
.onAppear {
|
|
withOptionalAnimation(reduceMotion: reduceMotion) {
|
|
animateCards = true
|
|
}
|
|
}
|
|
}
|
|
|
|
private func toggleSelection(_ time: OnboardingTimePreference) {
|
|
withOptionalAnimation(.easeInOut(duration: Design.Animation.quick), reduceMotion: reduceMotion) {
|
|
if selectedTimes.contains(time) {
|
|
selectedTimes.remove(time)
|
|
} else {
|
|
selectedTimes.insert(time)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A single time preference card.
|
|
private struct TimeCardView: View {
|
|
let time: OnboardingTimePreference
|
|
let isSelected: Bool
|
|
let onTap: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: onTap) {
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
// Icon
|
|
SymbolIcon(time.symbolName, size: .rowContainer, color: isSelected ? AppAccent.primary : AppTextColors.secondary)
|
|
.frame(width: Design.Size.actionRowMinHeight)
|
|
|
|
// Text
|
|
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
|
Text(time.displayName)
|
|
.typography(.heading)
|
|
.foregroundStyle(AppTextColors.primary)
|
|
|
|
Text(time.subtitle)
|
|
.typography(.subheading)
|
|
.foregroundStyle(AppTextColors.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Selection indicator
|
|
if isSelected {
|
|
SymbolIcon("checkmark.circle.fill", size: .rowContainer, color: AppAccent.primary)
|
|
}
|
|
}
|
|
.padding(Design.Spacing.large)
|
|
.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)
|
|
.contentShape(.rect)
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityAddTraits(.isButton)
|
|
.accessibilityLabel(time.displayName)
|
|
.accessibilityHint(time.subtitle)
|
|
.accessibilityIdentifier("onboarding.time.\(time.rawValue)")
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ZStack {
|
|
LinearGradient(
|
|
colors: [AppSurface.primary, AppSurface.secondary],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
.ignoresSafeArea()
|
|
|
|
TimeSelectionStepView(
|
|
selectedTimes: .constant([]),
|
|
onContinue: {}
|
|
)
|
|
}
|
|
}
|