Andromida/Andromida/App/Views/Rituals/Sheets/RitualEditSheet.swift

698 lines
32 KiB
Swift

import SwiftUI
import Bedrock
/// Sheet for creating or editing a ritual
struct RitualEditSheet: View {
@Bindable var store: RitualStore
@Bindable var categoryStore: CategoryStore
let ritual: Ritual?
@Environment(\.dismiss) private var dismiss
// Form state
@State private var title: String = ""
@State private var theme: String = ""
@State private var notes: String = ""
@State private var durationDays: Double = 28
@State private var timeOfDay: TimeOfDay = .anytime
@State private var iconName: String = "sparkles"
@State private var selectedCategory: String = ""
@State private var habits: [EditableHabit] = []
@State private var showingIconPicker = false
@State private var showingHabitIconPicker = false
@State private var editingHabitIndex: Int?
@State private var newHabitTitle: String = ""
@State private var newHabitIcon: String = "circle.fill"
private var isEditing: Bool { ritual != nil }
private var canSave: Bool {
!title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
init(store: RitualStore, categoryStore: CategoryStore, ritual: Ritual?) {
self.store = store
self.categoryStore = categoryStore
self.ritual = ritual
}
var body: some View {
NavigationStack {
Form {
// Basic info section
basicInfoSection
// Schedule section
scheduleSection
// Habits section
habitsSection
// Notes section
notesSection
}
.environment(\.editMode, .constant(.active))
.scrollContentBackground(.hidden)
.background(AppSurface.primary)
.navigationTitle(isEditing ? String(localized: "Edit Ritual") : String(localized: "New Ritual"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(String(localized: "Cancel")) {
dismiss()
}
.foregroundStyle(AppTextColors.secondary)
}
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "Save")) {
saveRitual()
dismiss()
}
.foregroundStyle(AppAccent.primary)
.disabled(!canSave)
}
}
.onAppear {
loadExistingData()
}
.sheet(isPresented: $showingIconPicker) {
IconPickerSheet(selectedIcon: $iconName)
}
.sheet(isPresented: $showingHabitIconPicker) {
HabitIconPickerSheet(
selectedIcon: editingHabitIndex != nil
? Binding(
get: { habits[editingHabitIndex!].symbolName },
set: { habits[editingHabitIndex!].symbolName = $0 }
)
: $newHabitIcon
)
}
}
.presentationDetents([.large])
.presentationDragIndicator(.visible)
}
// MARK: - Form Sections
private var basicInfoSection: some View {
Section {
HStack(spacing: Design.Spacing.medium) {
Button {
showingIconPicker = true
} label: {
Image(systemName: iconName)
.font(.title)
.foregroundStyle(AppAccent.primary)
.frame(width: 44, height: 44)
.background(AppSurface.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
}
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
TextField(String(localized: "Ritual name"), text: $title)
.font(.headline)
TextField(String(localized: "Theme or tagline"), text: $theme)
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
}
}
.listRowBackground(AppSurface.card)
// Category selection - simple picker from CategoryStore
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
Text(String(localized: "Category"))
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
// Horizontal scrollable category chips
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Design.Spacing.small) {
// None option
categoryChip(
label: String(localized: "None"),
color: nil,
isSelected: selectedCategory.isEmpty
) {
selectedCategory = ""
}
// All categories from store
ForEach(categoryStore.categories) { cat in
categoryChip(
label: cat.name,
color: cat.color,
isSelected: selectedCategory == cat.name
) {
selectedCategory = cat.name
}
}
}
}
}
.listRowBackground(AppSurface.card)
} header: {
Text(String(localized: "Details"))
}
}
@State private var isEditingDuration: Bool = false
@State private var customDurationText: String = ""
private var scheduleSection: some View {
Section {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Picker(String(localized: "Time of Day"), selection: $timeOfDay) {
ForEach(TimeOfDay.allCases, id: \.self) { time in
Label(time.displayName, systemImage: time.symbolName)
.tag(time)
}
}
// Show the time range for the selected time of day
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: timeOfDay.symbolName)
.font(.caption)
Text(timeOfDay.timeRange)
.font(.caption)
}
.foregroundStyle(AppTextColors.tertiary)
}
.listRowBackground(AppSurface.card)
VStack(alignment: .leading, spacing: Design.Spacing.small) {
HStack {
Text(String(localized: "Duration"))
Spacer()
if isEditingDuration {
HStack(spacing: Design.Spacing.xSmall) {
TextField("", text: $customDurationText)
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing)
.frame(width: 60)
.textFieldStyle(.roundedBorder)
.onSubmit {
applyCustomDuration()
}
Text(String(localized: "days"))
.foregroundStyle(AppTextColors.secondary)
Button {
applyCustomDuration()
} label: {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(AppAccent.primary)
}
.buttonStyle(.plain)
}
} else {
Button {
customDurationText = "\(Int(durationDays))"
isEditingDuration = true
} label: {
HStack(spacing: Design.Spacing.xSmall) {
Text(String(localized: "\(Int(durationDays)) days"))
.foregroundStyle(AppTextColors.secondary)
Image(systemName: "pencil.circle")
.font(.caption)
.foregroundStyle(AppAccent.primary)
}
}
.buttonStyle(.plain)
}
}
if !isEditingDuration {
Slider(value: $durationDays, in: 7...365, step: 1)
.tint(AppAccent.primary)
// Quick duration presets
HStack(spacing: Design.Spacing.small) {
ForEach([14, 21, 28, 30, 90], id: \.self) { days in
Button {
durationDays = Double(days)
} label: {
Text("\(days)")
.font(.caption)
.foregroundStyle(Int(durationDays) == days ? AppAccent.primary : AppTextColors.tertiary)
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall)
.background(Int(durationDays) == days ? AppAccent.primary.opacity(0.2) : AppSurface.secondary)
.clipShape(.capsule)
}
.buttonStyle(.plain)
}
Spacer()
}
}
}
.listRowBackground(AppSurface.card)
} header: {
Text(String(localized: "Schedule"))
} footer: {
Text(String(localized: "Tap the duration to enter a custom number of days (up to 365)."))
.font(.caption)
}
}
private func applyCustomDuration() {
if let value = Int(customDurationText), value >= 1, value <= 365 {
durationDays = Double(value)
}
isEditingDuration = false
}
private var habitsSection: some View {
Section {
ForEach(Array(habits.enumerated()), id: \.element.id) { index, habit in
HStack(spacing: Design.Spacing.small) {
// Drag handle
Image(systemName: "line.3.horizontal")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
.frame(width: 20)
// Icon picker button
Button {
editingHabitIndex = index
showingHabitIconPicker = true
} label: {
Image(systemName: habit.symbolName)
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
}
.buttonStyle(.plain)
TextField(String(localized: "Habit name"), text: $habits[index].title)
Button {
habits.removeAll { $0.id == habit.id }
} label: {
Image(systemName: "minus.circle.fill")
.foregroundStyle(AppStatus.error)
}
.buttonStyle(.plain)
}
.listRowBackground(AppSurface.card)
}
.onMove { from, to in
habits.move(fromOffsets: from, toOffset: to)
}
// Add new habit row
HStack(spacing: Design.Spacing.medium) {
Button {
editingHabitIndex = nil
showingHabitIconPicker = true
} label: {
Image(systemName: newHabitIcon)
.foregroundStyle(AppTextColors.tertiary)
.frame(width: 24)
}
.buttonStyle(.plain)
TextField(String(localized: "Add a habit..."), text: $newHabitTitle)
.onSubmit {
addNewHabit()
}
Button {
addNewHabit()
} label: {
Image(systemName: "plus.circle.fill")
.foregroundStyle(AppAccent.primary)
}
.buttonStyle(.plain)
.disabled(newHabitTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
.listRowBackground(AppSurface.card)
} header: {
HStack {
Text(String(localized: "Habits"))
Spacer()
Text(String(localized: "\(habits.count) habits"))
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}
} footer: {
if habits.count > 1 {
Text(String(localized: "Drag the handle to reorder habits."))
.font(.caption)
}
}
}
private var notesSection: some View {
Section {
TextField(String(localized: "Add notes or reminders..."), text: $notes, axis: .vertical)
.lineLimit(3...6)
.listRowBackground(AppSurface.card)
} header: {
Text(String(localized: "Notes"))
}
}
// MARK: - Actions
private func loadExistingData() {
guard let ritual = ritual else { return }
title = ritual.title
theme = ritual.theme
notes = ritual.notes
durationDays = Double(ritual.durationDays)
timeOfDay = ritual.timeOfDay
iconName = ritual.iconName
habits = ritual.habits.map { EditableHabit(from: $0) }
// Load category
selectedCategory = ritual.category
}
private func addNewHabit() {
let trimmedTitle = newHabitTitle.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedTitle.isEmpty else { return }
habits.append(EditableHabit(title: trimmedTitle, symbolName: newHabitIcon))
newHabitTitle = ""
// Rotate to next icon
let icons = ["circle.fill", "star.fill", "heart.fill", "bolt.fill", "leaf.fill", "flame.fill"]
if let currentIndex = icons.firstIndex(of: newHabitIcon) {
newHabitIcon = icons[(currentIndex + 1) % icons.count]
}
}
private func categoryChip(
label: String,
color: Color?,
isSelected: Bool,
action: @escaping () -> Void
) -> some View {
Button(action: action) {
HStack(spacing: Design.Spacing.xSmall) {
if let color {
Circle()
.fill(color)
.frame(width: 10, height: 10)
}
Text(label)
.font(.subheadline)
}
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.background(isSelected ? (color ?? AppAccent.primary) : AppSurface.secondary)
.foregroundStyle(isSelected ? .white : AppTextColors.primary)
.clipShape(.capsule)
}
.buttonStyle(.plain)
}
private func saveRitual() {
let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedTheme = theme.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedNotes = notes.trimmingCharacters(in: .whitespacesAndNewlines)
if let existingRitual = ritual {
// Update existing ritual
store.updateRitual(
existingRitual,
title: trimmedTitle,
theme: trimmedTheme,
notes: trimmedNotes,
durationDays: Int(durationDays),
timeOfDay: timeOfDay,
iconName: iconName,
category: selectedCategory
)
// Update habits - remove old, add new
// Note: This is a simplified approach; a more sophisticated solution would diff the habits
for habit in existingRitual.habits {
store.removeHabit(habit, from: existingRitual)
}
for editableHabit in habits {
store.addHabit(to: existingRitual, title: editableHabit.title, symbolName: editableHabit.symbolName)
}
} else {
// Create new ritual
let newHabits = habits.enumerated().map { index, habit in
ArcHabit(title: habit.title, symbolName: habit.symbolName, sortIndex: index)
}
store.createRitual(
title: trimmedTitle,
theme: trimmedTheme,
notes: trimmedNotes,
durationDays: Int(durationDays),
timeOfDay: timeOfDay,
iconName: iconName,
category: selectedCategory,
habits: newHabits
)
}
}
}
/// A mutable habit for the edit form
private struct EditableHabit: Identifiable {
let id = UUID()
var title: String
var symbolName: String
init(title: String, symbolName: String) {
self.title = title
self.symbolName = symbolName
}
init(from habit: ArcHabit) {
self.title = habit.title
self.symbolName = habit.symbolName
}
}
/// Icon picker sheet for ritual icons
struct IconPickerSheet: View {
@Binding var selectedIcon: String
@Environment(\.dismiss) private var dismiss
@State private var searchText: String = ""
private let iconGroups: [(name: String, icons: [String])] = [
("Wellness", ["heart.fill", "heart.circle.fill", "drop.fill", "leaf.fill", "flame.fill", "bolt.fill", "pill.fill", "cross.fill", "stethoscope.circle.fill", "bandage.fill", "lungs.fill", "ear.fill"]),
("Time", ["sunrise.fill", "sun.max.fill", "sunset.fill", "moon.stars.fill", "moon.fill", "moon.zzz.fill", "clock.fill", "hourglass", "timer", "alarm.fill", "calendar", "calendar.badge.clock"]),
("Activity", ["figure.walk", "figure.run", "figure.mind.and.body", "figure.flexibility", "figure.cooldown", "figure.hiking", "figure.yoga", "figure.strengthtraining.traditional", "figure.dance", "figure.pool.swim", "dumbbell.fill", "sportscourt.fill", "bicycle"]),
("Mind", ["brain", "brain.head.profile", "sparkles", "star.fill", "lightbulb.fill", "eye.fill", "wand.and.stars", "hands.and.sparkles.fill", "person.and.background.dotted"]),
("Objects", ["book.fill", "book.closed.fill", "books.vertical.fill", "pencil.and.list.clipboard", "cup.and.saucer.fill", "mug.fill", "bed.double.fill", "tshirt.fill", "fork.knife", "gift.fill", "key.fill"]),
("Nature", ["tree.fill", "wind", "cloud.fill", "snowflake", "sun.horizon.fill", "moon.fill", "mountain.2.fill", "water.waves", "leaf.arrow.circlepath", "flower.fill"]),
("Home", ["house.fill", "bed.double.fill", "bathtub.fill", "shower.fill", "lamp.desk.fill", "chair.fill", "sofa.fill", "washer.fill"]),
("Work", ["briefcase.fill", "folder.fill", "doc.text.fill", "list.bullet.clipboard.fill", "checklist", "tray.full.fill", "chart.bar.fill", "gauge.with.dots.needle.bottom.50percent"]),
("Social", ["person.fill", "person.2.fill", "person.wave.2.fill", "bubble.left.fill", "phone.fill", "envelope.fill", "hand.wave.fill", "face.smiling.fill"]),
("Actions", ["checkmark.circle.fill", "target", "scope", "hand.thumbsup.fill", "hand.raised.fill", "bell.fill", "megaphone.fill", "flag.fill", "pin.fill"]),
("Finance", ["banknote.fill", "dollarsign.circle.fill", "creditcard.fill", "cart.fill", "bag.fill"]),
("Travel", ["airplane", "car.fill", "tram.fill", "ferry.fill", "mappin.circle.fill", "globe.americas.fill"]),
("Art", ["paintpalette.fill", "paintbrush.fill", "pencil.tip.crop.circle", "scissors", "theatermasks.fill", "music.note", "guitars.fill"]),
("Tech", ["iphone", "desktopcomputer", "keyboard.fill", "headphones", "wifi", "antenna.radiowaves.left.and.right", "camera.fill", "photo.fill"]),
("Arrows", ["arrow.up.circle.fill", "arrow.down.circle.fill", "arrow.counterclockwise.circle.fill", "arrow.2.circlepath", "arrow.triangle.2.circlepath"])
]
private var allIcons: [String] {
iconGroups.flatMap { $0.icons }
}
private var filteredIcons: [String] {
if searchText.isEmpty {
return allIcons
}
return allIcons.filter { $0.localizedStandardContains(searchText) }
}
var body: some View {
NavigationStack {
ScrollView {
if searchText.isEmpty {
// Show grouped icons
LazyVStack(alignment: .leading, spacing: Design.Spacing.large) {
ForEach(iconGroups, id: \.name) { group in
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(group.name)
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
.padding(.horizontal, Design.Spacing.small)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: Design.Spacing.small) {
ForEach(group.icons, id: \.self) { icon in
iconButton(icon)
}
}
}
}
}
.padding(Design.Spacing.large)
} else {
// Show filtered results
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: Design.Spacing.small) {
ForEach(filteredIcons, id: \.self) { icon in
iconButton(icon)
}
}
.padding(Design.Spacing.large)
}
}
.background(AppSurface.primary)
.searchable(text: $searchText, prompt: String(localized: "Search icons"))
.navigationTitle(String(localized: "Choose Icon"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "Done")) {
dismiss()
}
.foregroundStyle(AppAccent.primary)
}
}
}
.presentationDetents([.large])
.presentationDragIndicator(.visible)
}
private func iconButton(_ icon: String) -> some View {
Button {
selectedIcon = icon
dismiss()
} label: {
Image(systemName: icon)
.font(.title3)
.foregroundStyle(selectedIcon == icon ? AppAccent.primary : AppTextColors.secondary)
.frame(width: 44, height: 44)
.background(selectedIcon == icon ? AppAccent.primary.opacity(0.2) : AppSurface.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
}
.buttonStyle(.plain)
}
}
/// Icon picker sheet for habit icons with extensive SF Symbols
struct HabitIconPickerSheet: View {
@Binding var selectedIcon: String
@Environment(\.dismiss) private var dismiss
@State private var searchText: String = ""
private let iconGroups: [(name: String, icons: [String])] = [
("Common", ["circle.fill", "star.fill", "heart.fill", "bolt.fill", "leaf.fill", "flame.fill", "drop.fill", "sparkles", "checkmark.circle.fill", "target"]),
("Wellness", ["heart.fill", "heart.circle.fill", "cross.fill", "pill.fill", "stethoscope.circle.fill", "bandage.fill", "medical.thermometer.fill", "lungs.fill", "waveform.path.ecg", "face.smiling.fill"]),
("Fitness", ["figure.walk", "figure.run", "figure.mind.and.body", "figure.flexibility", "figure.cooldown", "figure.hiking", "figure.yoga", "figure.strengthtraining.traditional", "figure.dance", "figure.pool.swim", "figure.outdoor.cycle", "dumbbell.fill", "sportscourt.fill", "bicycle", "tennis.racket", "basketball.fill", "soccerball"]),
("Food & Drink", ["drop.fill", "cup.and.saucer.fill", "mug.fill", "fork.knife", "carrot.fill", "fish.fill", "leaf.fill", "takeoutbag.and.cup.and.straw.fill", "birthday.cake.fill", "wineglass.fill"]),
("Mind", ["brain", "brain.head.profile", "lightbulb.fill", "eye.fill", "ear.fill", "sparkles", "wand.and.stars", "hands.and.sparkles.fill", "person.and.background.dotted", "moon.zzz.fill"]),
("Reading & Writing", ["book.fill", "book.closed.fill", "books.vertical.fill", "text.book.closed.fill", "pencil", "pencil.and.list.clipboard", "note.text", "doc.text.fill", "newspaper.fill", "bookmark.fill"]),
("Time", ["clock.fill", "alarm.fill", "timer", "hourglass", "sunrise.fill", "sunset.fill", "moon.fill", "moon.stars.fill", "calendar", "calendar.badge.clock"]),
("Home", ["bed.double.fill", "bathtub.fill", "shower.fill", "washer.fill", "house.fill", "lamp.desk.fill", "chair.fill", "sofa.fill", "lightbulb.fill", "fan.fill"]),
("Work", ["briefcase.fill", "folder.fill", "tray.full.fill", "archivebox.fill", "calendar", "calendar.badge.plus", "checkmark.square.fill", "list.bullet.clipboard.fill", "checklist", "chart.bar.fill", "gauge.with.dots.needle.bottom.50percent", "laptopcomputer"]),
("Social", ["person.fill", "person.2.fill", "person.wave.2.fill", "bubble.left.fill", "phone.fill", "envelope.fill", "hand.wave.fill", "message.fill", "video.fill", "hand.thumbsup.fill"]),
("Nature", ["sun.max.fill", "cloud.fill", "wind", "snowflake", "tree.fill", "flower.fill", "mountain.2.fill", "water.waves", "leaf.arrow.circlepath", "globe.americas.fill"]),
("Tech", ["iphone", "desktopcomputer", "keyboard.fill", "headphones", "wifi", "antenna.radiowaves.left.and.right", "laptopcomputer", "applewatch", "airpods", "gamecontroller.fill"]),
("Finance", ["banknote.fill", "dollarsign.circle.fill", "creditcard.fill", "cart.fill", "bag.fill", "chart.line.uptrend.xyaxis", "building.columns.fill"]),
("Travel", ["airplane", "car.fill", "tram.fill", "ferry.fill", "mappin.circle.fill", "globe.americas.fill", "suitcase.fill", "fuelpump.fill"]),
("Art & Music", ["paintpalette.fill", "paintbrush.fill", "pencil.tip.crop.circle", "scissors", "theatermasks.fill", "music.note", "guitars.fill", "pianokeys", "headphones", "film.fill"]),
("Self-Care", ["comb.fill", "eyebrow", "lips.fill", "hand.raised.fingers.spread.fill", "sparkles", "shower.fill", "bathtub.fill", "leaf.fill"]),
("Cleaning", ["trash.fill", "archivebox.fill", "tshirt.fill", "washer.fill", "sparkles", "bubble.left.and.bubble.right.fill"]),
("Status", ["checkmark.circle.fill", "xmark.circle.fill", "exclamationmark.circle.fill", "questionmark.circle.fill", "target", "scope", "flag.fill", "rosette"]),
("Shapes", ["circle.fill", "square.fill", "triangle.fill", "diamond.fill", "hexagon.fill", "octagon.fill", "seal.fill", "staroflife.fill"]),
("Arrows", ["arrow.up.circle.fill", "arrow.down.circle.fill", "arrow.left.circle.fill", "arrow.right.circle.fill", "arrow.counterclockwise.circle.fill", "arrow.2.circlepath", "arrow.triangle.2.circlepath"]),
("Misc", ["gift.fill", "ticket.fill", "tag.fill", "pin.fill", "mappin.circle.fill", "key.fill", "lock.fill", "bell.fill", "megaphone.fill", "music.note", "photo.fill", "camera.fill", "puzzlepiece.fill", "dice.fill"])
]
private var allIcons: [String] {
// Remove duplicates while maintaining order
var seen = Set<String>()
return iconGroups.flatMap { $0.icons }.filter { seen.insert($0).inserted }
}
private var filteredIcons: [String] {
if searchText.isEmpty {
return allIcons
}
return allIcons.filter { $0.localizedStandardContains(searchText) }
}
var body: some View {
NavigationStack {
ScrollView {
if searchText.isEmpty {
// Show grouped icons
LazyVStack(alignment: .leading, spacing: Design.Spacing.large) {
ForEach(iconGroups, id: \.name) { group in
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(group.name)
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
.padding(.horizontal, Design.Spacing.small)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: Design.Spacing.small) {
ForEach(group.icons, id: \.self) { icon in
iconButton(icon)
}
}
}
}
}
.padding(Design.Spacing.large)
} else {
// Show filtered results
if filteredIcons.isEmpty {
ContentUnavailableView(
String(localized: "No icons found"),
systemImage: "magnifyingglass",
description: Text(String(localized: "Try a different search term"))
)
} else {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: Design.Spacing.small) {
ForEach(filteredIcons, id: \.self) { icon in
iconButton(icon)
}
}
.padding(Design.Spacing.large)
}
}
}
.background(AppSurface.primary)
.searchable(text: $searchText, prompt: String(localized: "Search icons (e.g., heart, star, book)"))
.navigationTitle(String(localized: "Choose Habit Icon"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "Done")) {
dismiss()
}
.foregroundStyle(AppAccent.primary)
}
}
}
.presentationDetents([.large])
.presentationDragIndicator(.visible)
}
private func iconButton(_ icon: String) -> some View {
Button {
selectedIcon = icon
dismiss()
} label: {
Image(systemName: icon)
.font(.title3)
.foregroundStyle(selectedIcon == icon ? AppAccent.primary : AppTextColors.secondary)
.frame(width: 44, height: 44)
.background(selectedIcon == icon ? AppAccent.primary.opacity(0.2) : AppSurface.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
}
.buttonStyle(.plain)
}
}
#Preview {
RitualEditSheet(store: RitualStore.preview, categoryStore: CategoryStore.preview, ritual: nil)
}