298 lines
11 KiB
Swift
298 lines
11 KiB
Swift
import SwiftUI
|
|
import Bedrock
|
|
|
|
/// Sheet for browsing and adding preset rituals
|
|
struct PresetLibrarySheet: View {
|
|
@Bindable var store: RitualStore
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
@State private var selectedCategory: PresetCategory = .health
|
|
@State private var selectedPreset: RitualPreset?
|
|
|
|
/// Track which presets have been added (by title match)
|
|
private var addedPresetTitles: Set<String> {
|
|
Set(store.rituals.map { $0.title })
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(spacing: 0) {
|
|
// Category picker
|
|
categoryPicker
|
|
|
|
// Presets list
|
|
ScrollView(.vertical, showsIndicators: false) {
|
|
VStack(spacing: Design.Spacing.medium) {
|
|
ForEach(RitualPresetLibrary.presets(for: selectedCategory)) { preset in
|
|
presetCard(for: preset)
|
|
}
|
|
}
|
|
.padding(Design.Spacing.large)
|
|
}
|
|
}
|
|
.background(AppSurface.primary)
|
|
.navigationTitle(String(localized: "Preset Library"))
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button(String(localized: "Done")) {
|
|
dismiss()
|
|
}
|
|
.foregroundStyle(AppAccent.primary)
|
|
}
|
|
}
|
|
.sheet(item: $selectedPreset) { preset in
|
|
PresetDetailSheet(store: store, preset: preset, isAlreadyAdded: addedPresetTitles.contains(preset.title))
|
|
}
|
|
}
|
|
.presentationDetents([.large])
|
|
.presentationDragIndicator(.visible)
|
|
}
|
|
|
|
// MARK: - Category Picker
|
|
|
|
private var categoryPicker: some View {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: Design.Spacing.small) {
|
|
ForEach(PresetCategory.allCases, id: \.self) { category in
|
|
Button {
|
|
withAnimation(.easeInOut(duration: Design.Animation.quick)) {
|
|
selectedCategory = category
|
|
}
|
|
} label: {
|
|
HStack(spacing: Design.Spacing.xSmall) {
|
|
Image(systemName: category.symbolName)
|
|
Text(category.displayName)
|
|
}
|
|
.font(.subheadline)
|
|
.padding(.horizontal, Design.Spacing.medium)
|
|
.padding(.vertical, Design.Spacing.small)
|
|
.background(selectedCategory == category ? AppAccent.primary : AppSurface.card)
|
|
.foregroundStyle(selectedCategory == category ? .white : AppTextColors.primary)
|
|
.clipShape(.capsule)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
.padding(.vertical, Design.Spacing.medium)
|
|
}
|
|
.background(AppSurface.secondary)
|
|
}
|
|
|
|
// MARK: - Preset Card
|
|
|
|
private func presetCard(for preset: RitualPreset) -> some View {
|
|
let isAdded = addedPresetTitles.contains(preset.title)
|
|
|
|
return Button {
|
|
selectedPreset = preset
|
|
} label: {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
Image(systemName: preset.iconName)
|
|
.font(.title2)
|
|
.foregroundStyle(AppAccent.primary)
|
|
.frame(width: 40, height: 40)
|
|
.background(AppAccent.primary.opacity(0.1))
|
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
|
|
|
|
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
|
HStack {
|
|
Text(preset.title)
|
|
.font(.headline)
|
|
.foregroundStyle(AppTextColors.primary)
|
|
|
|
if isAdded {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.foregroundStyle(AppStatus.success)
|
|
.font(.caption)
|
|
}
|
|
}
|
|
|
|
Text(preset.theme)
|
|
.font(.subheadline)
|
|
.foregroundStyle(AppTextColors.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
VStack(alignment: .trailing, spacing: Design.Spacing.xSmall) {
|
|
Image(systemName: preset.timeOfDay.symbolName)
|
|
.font(.caption)
|
|
.foregroundStyle(AppTextColors.tertiary)
|
|
|
|
Text(String(localized: "\(preset.habits.count) habits"))
|
|
.font(.caption)
|
|
.foregroundStyle(AppTextColors.tertiary)
|
|
}
|
|
}
|
|
|
|
// Habit preview
|
|
HStack(spacing: Design.Spacing.small) {
|
|
ForEach(preset.habits.prefix(4)) { habit in
|
|
Image(systemName: habit.symbolName)
|
|
.font(.caption)
|
|
.foregroundStyle(AppTextColors.tertiary)
|
|
}
|
|
|
|
if preset.habits.count > 4 {
|
|
Text("+\(preset.habits.count - 4)")
|
|
.font(.caption2)
|
|
.foregroundStyle(AppTextColors.tertiary)
|
|
}
|
|
}
|
|
}
|
|
.padding(Design.Spacing.large)
|
|
.background(AppSurface.card)
|
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
/// Detail sheet for a single preset with full info and add button
|
|
struct PresetDetailSheet: View {
|
|
@Bindable var store: RitualStore
|
|
let preset: RitualPreset
|
|
let isAlreadyAdded: Bool
|
|
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var hasBeenAdded = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ScrollView(.vertical, showsIndicators: false) {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.xLarge) {
|
|
// Header
|
|
headerSection
|
|
|
|
// Description
|
|
if !preset.notes.isEmpty {
|
|
descriptionSection
|
|
}
|
|
|
|
// Habits
|
|
habitsSection
|
|
|
|
// Add button
|
|
addButton
|
|
|
|
Spacer(minLength: Design.Spacing.xxxLarge)
|
|
}
|
|
.padding(Design.Spacing.large)
|
|
}
|
|
.background(AppSurface.primary)
|
|
.navigationTitle(preset.title)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button(String(localized: "Done")) {
|
|
dismiss()
|
|
}
|
|
.foregroundStyle(AppAccent.primary)
|
|
}
|
|
}
|
|
}
|
|
.presentationDetents([.large])
|
|
.presentationDragIndicator(.visible)
|
|
}
|
|
|
|
private var headerSection: some View {
|
|
VStack(spacing: Design.Spacing.medium) {
|
|
SymbolIcon(preset.iconName, size: .hero, color: AppAccent.primary)
|
|
|
|
Text(preset.theme)
|
|
.typography(.body)
|
|
.foregroundStyle(AppTextColors.secondary)
|
|
|
|
HStack(spacing: Design.Spacing.large) {
|
|
Label(preset.timeOfDay.displayName, systemImage: preset.timeOfDay.symbolName)
|
|
Label(String(localized: "\(preset.durationDays) days"), systemImage: "calendar")
|
|
}
|
|
.typography(.caption)
|
|
.foregroundStyle(AppTextColors.tertiary)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
|
|
private var descriptionSection: some View {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
|
Text(String(localized: "About"))
|
|
.font(.headline)
|
|
.foregroundStyle(AppTextColors.primary)
|
|
|
|
Text(preset.notes)
|
|
.font(.body)
|
|
.foregroundStyle(AppTextColors.secondary)
|
|
}
|
|
.padding(Design.Spacing.large)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(AppSurface.card)
|
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
|
}
|
|
|
|
private var habitsSection: some View {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
|
Text(String(localized: "Habits"))
|
|
.font(.headline)
|
|
.foregroundStyle(AppTextColors.primary)
|
|
|
|
VStack(spacing: Design.Spacing.xSmall) {
|
|
ForEach(preset.habits) { habit in
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
Image(systemName: habit.symbolName)
|
|
.foregroundStyle(AppAccent.primary)
|
|
.frame(width: 24)
|
|
|
|
Text(habit.title)
|
|
.font(.subheadline)
|
|
.foregroundStyle(AppTextColors.primary)
|
|
|
|
Spacer()
|
|
}
|
|
.padding(.vertical, Design.Spacing.small)
|
|
|
|
if habit.id != preset.habits.last?.id {
|
|
Divider()
|
|
.background(AppBorder.subtle)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(Design.Spacing.large)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(AppSurface.card)
|
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
|
}
|
|
|
|
private var addButton: some View {
|
|
Button {
|
|
store.createRitualFromPreset(preset)
|
|
hasBeenAdded = true
|
|
} label: {
|
|
HStack {
|
|
if isAlreadyAdded || hasBeenAdded {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
Text(String(localized: "Added to My Rituals"))
|
|
} else {
|
|
Image(systemName: "plus.circle.fill")
|
|
Text(String(localized: "Add to My Rituals"))
|
|
}
|
|
}
|
|
.font(.headline)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(Design.Spacing.medium)
|
|
.background(isAlreadyAdded || hasBeenAdded ? AppStatus.success : AppAccent.primary)
|
|
.foregroundStyle(.white)
|
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
|
}
|
|
.buttonStyle(.plain)
|
|
.disabled(isAlreadyAdded || hasBeenAdded)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
PresetLibrarySheet(store: RitualStore.preview)
|
|
}
|