Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
5f3f79e6be
commit
e5b6f49033
@ -8,6 +8,14 @@
|
||||
"comment" : "A text view displaying the day number in a history calendar cell. The text is centered and has a small font size.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"%lld days" : {
|
||||
"comment" : "A label displaying the duration of a preset in days. The argument is the number of days the preset is active.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"%lld habits" : {
|
||||
"comment" : "A label showing the number of habits included in a ritual preset. The argument is the count of habits in the preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"%lld of %lld" : {
|
||||
"comment" : "A title that shows the number of habits completed on a specific day, followed by a label that describes what that number represents. The first argument is the count of completed habits. The second argument is a string that describes the nature of the habits being counted.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@ -50,10 +58,26 @@
|
||||
"comment" : "A text label displaying a percentage. The argument is a value between 0.0 and 1.0.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"+%lld" : {
|
||||
"comment" : "A label that appears in the preset card to indicate that there are more habits than can be shown in the preview. The number inside the plus sign is the count of additional habits.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"5-minute meditation" : {
|
||||
"comment" : "Title of a habit preset within a mindfulness ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"7-Day Trend" : {
|
||||
"comment" : "A heading for the 7-day trend section of an insight detail sheet.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"A calm mind sets the tone for a calm day." : {
|
||||
"comment" : "Notes for the \"Morning Meditation\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"A consistent skincare routine for healthy skin." : {
|
||||
"comment" : "Notes section for the \"Morning Skincare\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"A fresh ritual created from your focus today." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -174,6 +198,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Add a habit..." : {
|
||||
"comment" : "A placeholder text for a text field that allows users to input the name of a new habit.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Add notes or reminders..." : {
|
||||
"comment" : "A placeholder text for a text field where the user can add notes or reminders.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Add to My Rituals" : {
|
||||
"comment" : "A button label that says \"Add to My Rituals\".",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Added to My Rituals" : {
|
||||
"comment" : "A label indicating that a preset has been successfully added to the user's rituals.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Adjust arc duration" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -204,6 +244,30 @@
|
||||
"comment" : "Title for the \"All\" option in the history ritual filter picker.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Anytime" : {
|
||||
"comment" : "Name of the \"Anytime\" time of day option in the Ritual editor.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Apply moisturizer" : {
|
||||
"comment" : "Name of a habit within a Self-Care Ritual Preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Apply sunscreen" : {
|
||||
"comment" : "Name of a habit within a self-care ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Archive" : {
|
||||
"comment" : "A button that archives a ritual, hiding it from the user's active list but preserving its history.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Archive Ritual?" : {
|
||||
"comment" : "A confirmation prompt title for archiving a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Archived" : {
|
||||
"comment" : "A label displayed above the user's archived rituals.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Balanced daily check-ins" : {
|
||||
"comment" : "Description of what the \"Steady\" focus style means for the user.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -230,6 +294,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Body scan for tension" : {
|
||||
"comment" : "Habit title for a mindfulness ritual that involves scanning the body for tension.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Box breathing (4-4-4-4)" : {
|
||||
"comment" : "Description of a habit within a ritual preset, focusing on a specific breathing technique.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Branding Preview" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -252,10 +324,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Break up your day" : {
|
||||
"comment" : "Notes for a ritual preset focused on breaking up a busy day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Breakdown" : {
|
||||
"comment" : "A label displayed above the breakdown of a user's insights.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Breathwork" : {
|
||||
"comment" : "Title of a ritual preset focused on using breath to reduce stress and increase focus.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Browse Presets" : {
|
||||
"comment" : "A button that, when tapped, presents a sheet displaying a list of available ritual presets.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Build the habit of hydrating first thing in the morning." : {
|
||||
"comment" : "Notes section of a ritual preset focused on morning hydration.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Calm your nervous system" : {
|
||||
"comment" : "Description of a habit within a ritual preset focused on mindfulness.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Cancel" : {
|
||||
"comment" : "The \"Delete\" button in the confirmation alert for a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Care for yourself" : {
|
||||
"comment" : "Theme of the \"Morning Skincare\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Category" : {
|
||||
"comment" : "A label for the category of a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Change into comfortable clothes" : {
|
||||
"comment" : "Habit title for a ritual preset that encourages dressing in comfortable clothes for the evening.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Check-ins completed" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
@ -301,6 +409,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Choose Icon" : {
|
||||
"comment" : "The title of the icon picker sheet.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Choose ONE thing to focus on" : {
|
||||
"comment" : "Title of a habit within a \"Focus Reset\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Choose the intensity of your arc" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -323,8 +439,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Cleanse face" : {
|
||||
"comment" : "Title of a habit within a Self-Care Ritual Preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Clear All Completions" : {
|
||||
|
||||
},
|
||||
"Clear your desk" : {
|
||||
"comment" : "Title of a habit preset for a productivity ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Clear your inbox" : {
|
||||
"comment" : "Habit title for reviewing and clearing the user's inbox.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Close loops, plan ahead" : {
|
||||
"comment" : "Note for a ritual preset focused on closing daily tasks and planning for the next day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Close the day with awareness and intention." : {
|
||||
"comment" : "Theme of the \"Evening Reflection\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Close unnecessary tabs" : {
|
||||
"comment" : "Title of a habit in a ritual preset focused on productivity, related to closing unnecessary tabs on a device.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Combat sedentary habits with midday activity." : {
|
||||
"comment" : "Notes for a ritual preset focused on breaking up a day with midday activity.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Coming Soon" : {
|
||||
"comment" : "A label indicating that a feature is coming soon.",
|
||||
@ -380,11 +524,27 @@
|
||||
},
|
||||
"Consecutive perfect days" : {
|
||||
|
||||
},
|
||||
"Create" : {
|
||||
"comment" : "A button label that says \"Create\".",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Create a buffer between your day and sleep." : {
|
||||
"comment" : "Notes for the \"Evening Wind-Down\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Create a custom ritual or browse our preset library to get started." : {
|
||||
"comment" : "A description below the buttons that allow users to create a custom ritual or view the preset library.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Create as many arcs as you need" : {
|
||||
"comment" : "Subtitle for a feature row in the \"Rituals Pro\" upgrade view, describing the ability to create as many rituals as needed.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Create New" : {
|
||||
"comment" : "A button label that suggests creating a new ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Create ritual" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -407,6 +567,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Create the conditions for uninterrupted deep work." : {
|
||||
"comment" : "Notes section of a productivity ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Creates a new ritual" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -525,12 +689,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Days" : {
|
||||
"comment" : "Title of an insight card showing the total number of days the user has been working on their rituals.",
|
||||
"Days Active" : {
|
||||
"comment" : "Title of an insight card that shows the number of days the user has completed at least one habit. Each day a habit is checked in counts toward the total.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Days on your journey" : {
|
||||
"comment" : "Title of an insight card that shows the total number of days a user has been working on their rituals.",
|
||||
"Days you checked in" : {
|
||||
"comment" : "Title of an insight card that shows the number of days the user has completed at least one habit. Each day a habit is checked in counts toward the user's journey.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Debug" : {
|
||||
@ -555,10 +719,50 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Deep Work Prep" : {
|
||||
"comment" : "Title of a ritual preset focused on setting up for focused work.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Deeper analytics on your progress" : {
|
||||
"comment" : "Subtitle for a feature row in the \"Pro\" upgrade view, describing advanced insights.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Define your top 3 priorities" : {
|
||||
"comment" : "Title of a habit within a ritual preset focused on productivity.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Delete" : {
|
||||
"comment" : "A destructive button that deletes a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Delete Ritual?" : {
|
||||
"comment" : "The title of an alert that asks if the user wants to delete a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Details" : {
|
||||
"comment" : "The header text for the \"Details\" section of the ritual edit sheet.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Digital Detox" : {
|
||||
"comment" : "Title of a ritual preset that encourages disconnecting from screens to reconnect.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Dim lights 1 hour before bed" : {
|
||||
"comment" : "Habit title for dimming lights one hour before bedtime.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Disable" : {
|
||||
"comment" : "A button label that disables a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Disabled" : {
|
||||
"comment" : "A label indicating that a feature or option is currently disabled.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Disconnect to reconnect" : {
|
||||
"comment" : "Theme for the \"Digital Detox\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Done" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -612,6 +816,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Drink a glass of water" : {
|
||||
"comment" : "Title of a habit within a preset ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Drink water" : {
|
||||
"comment" : "Habit title for drinking water.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Duration" : {
|
||||
"comment" : "A label displayed above the slider that controls the duration of a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Each ritual is a chapter. Build the cadence, then let the momentum carry you." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -634,6 +850,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Early bedtime" : {
|
||||
"comment" : "Habit title for a ritual preset that encourages going to bed early.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Eat a healthy breakfast" : {
|
||||
"comment" : "Title of a habit within a ritual preset, related to eating a healthy breakfast.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Edit" : {
|
||||
"comment" : "A button label that translates to \"Edit\".",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Edit Ritual" : {
|
||||
"comment" : "The title of the navigation bar for editing a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Enable" : {
|
||||
|
||||
},
|
||||
"End-of-Day Review" : {
|
||||
"comment" : "Title of a ritual preset that encourages reviewing completed tasks and planning for the next day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Evening" : {
|
||||
"comment" : "Description of a ritual scheduling option when the ritual should be visible in the Today view after 5pm.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Evening Reflection" : {
|
||||
"comment" : "Title of a ritual preset that encourages users to reflect on their day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Evening Reset" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -656,6 +903,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Evening Wind-Down" : {
|
||||
"comment" : "Title of a ritual preset that encourages transitioning to rest at the end of the day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Faster iCloud synchronization" : {
|
||||
|
||||
},
|
||||
@ -682,7 +933,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Find the good" : {
|
||||
"comment" : "Title of a habit in a ritual preset focused on practicing gratitude.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Focus Reset" : {
|
||||
"comment" : "Title of a ritual preset that encourages users to refocus when they feel scattered.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Focus ritual" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@ -731,6 +991,7 @@
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Four-week arc in progress" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@ -840,6 +1101,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Gentle stretching" : {
|
||||
"comment" : "Habit title for a ritual preset focused on self-care, emphasizing gentle stretching as a habit.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Get a gentle check-in each morning" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -862,6 +1127,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Give your mind a break from screens." : {
|
||||
"comment" : "Notes for a ritual preset focused on giving the mind a break from screens.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Gratitude Practice" : {
|
||||
"comment" : "Title of a ritual preset focused on practicing gratitude.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Habit name" : {
|
||||
"comment" : "A text field label for entering a habit name.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Habits" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -932,8 +1209,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Have a real conversation" : {
|
||||
"comment" : "Habit title for a ritual preset that encourages having a real conversation with someone.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Help us build more features" : {
|
||||
|
||||
},
|
||||
"Herbal tea" : {
|
||||
"comment" : "Habit title for a ritual preset that includes herbal tea as a habit.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"History" : {
|
||||
"comment" : "Title of the history view.",
|
||||
@ -1053,6 +1338,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Journal for 5 minutes" : {
|
||||
"comment" : "Habit title for a mindfulness ritual where the user writes in a journal for 5 minutes.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Keep room cool" : {
|
||||
"comment" : "Habit title for keeping the bedroom cool at night.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Last synced %@" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
@ -1076,10 +1369,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Let go of the day" : {
|
||||
"comment" : "Habit title for a mindfulness ritual where the user lets go of their worries or stresses of the day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Light a candle or dim lights" : {
|
||||
"comment" : "Habit within a RitualPreset related to creating a buffer between your day and sleep.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Longest streak" : {
|
||||
"comment" : "Label for the longest streak breakdown item.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Midday Movement" : {
|
||||
"comment" : "Title of a ritual preset that encourages regular physical activity during the day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Mindful minute" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1124,6 +1429,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Morning" : {
|
||||
"comment" : "Name of the time of day option for a ritual that appears in the Today view in the morning.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Morning Clarity" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1146,6 +1455,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Morning Hydration" : {
|
||||
"comment" : "Title of a ritual preset focused on starting the day refreshed by drinking a glass of water, taking vitamins, and eating a healthy breakfast.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Morning Meditation" : {
|
||||
"comment" : "Title of a ritual preset focused on mindfulness, designed to start the day with stillness.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Morning Skincare" : {
|
||||
"comment" : "Title of a ritual preset focused on skincare.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Move" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1168,6 +1489,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"New Ritual" : {
|
||||
"comment" : "The title of the view when creating a new ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"No caffeine after 2pm" : {
|
||||
"comment" : "Habit title for not consuming caffeine after 2 PM.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"No habits tracked" : {
|
||||
"comment" : "A message displayed when a user has not tracked any habits on a given day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -1194,6 +1523,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"No Rituals Yet" : {
|
||||
"comment" : "A message displayed when a user has not created any rituals yet.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"No screens" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1216,6 +1549,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"No screens 1 hour before bed" : {
|
||||
"comment" : "Habit title in a RitualPreset related to a \"Digital Detox\" ritual, encouraging users to avoid screens one hour before bedtime.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"None" : {
|
||||
"comment" : "A placeholder text for the \"Category\" picker when no category is selected.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Not completed" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1264,6 +1605,14 @@
|
||||
"comment" : "Subtitle text for the reminder toggle when notifications are disabled in settings.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Plan the week ahead" : {
|
||||
"comment" : "Habit title for a ritual preset that helps users plan their week ahead.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Plan tomorrow's priorities" : {
|
||||
"comment" : "Habit title for a Ritual Preset that helps users plan their day for the next day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Play subtle completion sounds" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1286,6 +1635,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Practice gratitude" : {
|
||||
"comment" : "Habit title for a ritual preset focused on practicing gratitude.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Preferences" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1310,6 +1663,22 @@
|
||||
},
|
||||
"Preload 6 Months Demo Data" : {
|
||||
|
||||
},
|
||||
"Prepare clothes for Monday" : {
|
||||
"comment" : "Habit within a ritual preset to prepare clothes for Monday.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Prepare for a fresh week" : {
|
||||
"comment" : "Title of a ritual preset that encourages users to plan and prepare for the upcoming week.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Preset Library" : {
|
||||
"comment" : "The title of the preset library sheet.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Presets" : {
|
||||
"comment" : "A button that navigates to the preset library.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Preview launch and icon" : {
|
||||
"localizations" : {
|
||||
@ -1340,6 +1709,10 @@
|
||||
"comment" : "A description below the button that says that the Pro features are in development.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Process your day" : {
|
||||
"comment" : "Theme for a ritual preset that encourages reflection and processing of the day's events.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Read 10 pages" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1362,6 +1735,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Read a physical book" : {
|
||||
"comment" : "Habit title for reading a physical book as part of a RitualPreset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Reflect" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1384,6 +1761,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Reflect on a positive moment" : {
|
||||
"comment" : "Habit title for a ritual preset that encourages reflecting on a positive moment from the day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Regain clarity" : {
|
||||
"comment" : "Description of a ritual preset that encourages users to refocus when they feel scattered.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Release shoulder tension" : {
|
||||
"comment" : "Habit title for releasing shoulder tension during mindfulness practice.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Reminder time" : {
|
||||
|
||||
},
|
||||
@ -1391,6 +1780,26 @@
|
||||
"comment" : "Title of a navigation row in the Settings view that resets the user's onboarding state.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Rest better tonight" : {
|
||||
"comment" : "Notes for a ritual preset focused on sleep preparation.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Restore" : {
|
||||
"comment" : "A button label that restores a deleted or archived ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Review completed tasks" : {
|
||||
"comment" : "Title of a habit in a RitualPreset related to reviewing completed tasks.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Review last week" : {
|
||||
"comment" : "Habit title for reviewing their habits from the previous week.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Review your day and set up tomorrow for success." : {
|
||||
"comment" : "Notes for a ritual preset focused on closing loops and planning ahead for the next day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Ritual" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1462,6 +1871,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Ritual name" : {
|
||||
"comment" : "A label for the name of a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Ritual pacing" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1558,6 +1971,30 @@
|
||||
"comment" : "A description of what \"Rituals Pro\" offers.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Save" : {
|
||||
"comment" : "The text for a button that saves data.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Schedule" : {
|
||||
"comment" : "A label displayed above the ritual's scheduling information.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Set an intention for the day" : {
|
||||
"comment" : "Habit title for a ritual preset focused on setting an intention for the day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Set consistent bedtime" : {
|
||||
"comment" : "Habit within a ritual preset to help users set a consistent bedtime.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Set phone to Do Not Disturb" : {
|
||||
"comment" : "Habit title for setting a phone to \"Do Not Disturb\" during a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Set up for focus" : {
|
||||
"comment" : "Theme of the \"Deep Work Prep\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Settings" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1580,6 +2017,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Shift your focus to what's going well." : {
|
||||
"comment" : "Notes for the \"Gratitude Practice\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Show less" : {
|
||||
"comment" : "A button label that indicates to collapse the history view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -1611,6 +2052,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Sleep Preparation" : {
|
||||
"comment" : "Theme of the \"Sleep Preparation\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Soft landings" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1655,6 +2100,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Start with stillness" : {
|
||||
"comment" : "Theme of the \"Morning Meditation\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Start your day refreshed" : {
|
||||
"comment" : "Theme for the \"Morning Hydration\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Start your first ritual" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1725,6 +2178,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Stretch at your desk" : {
|
||||
"comment" : "Habit title for a ritual preset focused on health, encouraging users to stretch while working at their desk.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Sunday evening ritual to start Monday strong." : {
|
||||
"comment" : "Description of a ritual preset that serves as a weekly reset to help users start their week on a positive note.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Support Development" : {
|
||||
"comment" : "Subtitle for a feature row in the \"Pro\" upgrade view, related to supporting the development of the app.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -1889,10 +2350,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Take 5 deep breaths" : {
|
||||
"comment" : "Habit title for a ritual preset focused on productivity, encouraging the user to take deep breaths.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Take a 10-minute walk" : {
|
||||
"comment" : "Title of a habit within a ritual preset focused on breaking up a busy day with midday activity.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Take a moment to check in on your daily habits." : {
|
||||
"comment" : "Body text for a daily reminder notification.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Take vitamins" : {
|
||||
"comment" : "Title of a habit within a ritual preset, translated to a language other than English.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Tap a habit to check in" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -1945,22 +2418,54 @@
|
||||
"comment" : "A hint that appears when a user taps on a day cell to explain that it will navigate to more details about that day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Thank someone" : {
|
||||
"comment" : "Habit title for a ritual preset focused on gratitude practice.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"The number of days you've completed at least one habit. Each day you check in counts toward your journey." : {
|
||||
"comment" : "Explanation for the InsightCard titled \"Days Active\".",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"The number of habits you've checked off today across all your active rituals. Each check-in builds momentum toward your goals." : {
|
||||
"comment" : "Explanation of the value for the Insight Card titled \"Habits today\".",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"The total number of days you've been working on your rituals. This shows your progress through each arc, combining all active rituals." : {
|
||||
"comment" : "Explanation of the \"Days\" insight card in the Ritual Insights section.",
|
||||
"Theme or tagline" : {
|
||||
"comment" : "A label for an optional tagline or theme associated with a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"This day has no habit data recorded." : {
|
||||
"comment" : "A description displayed when a day has no habit completion data.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"This ritual will be hidden from your active list but its history will be preserved." : {
|
||||
"comment" : "An alert message that appears when archiving a ritual. It informs the user that the ritual will remain in their history but will not be visible in the \"Active Rituals\" section.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"This will permanently remove \"%@\" and all its completion history. This cannot be undone." : {
|
||||
"comment" : "An alert message explaining that archiving a ritual will hide it from the user's active list but keep its completion history. The placeholder text inside the message is replaced with the name of the ritual that the user is trying",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"This will permanently remove this ritual and all its completion history. This cannot be undone." : {
|
||||
"comment" : "An alert message that appears when the user attempts to delete a ritual. It explains that deleting the ritual cannot be undone.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Tidy workspace" : {
|
||||
"comment" : "Habit within a ritual preset that encourages tidying one's workspace.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Tidy your space" : {
|
||||
"comment" : "Habit title for tidying one's space as part of a Self-Care Ritual Preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Time for your rituals" : {
|
||||
"comment" : "Title of a notification displayed at the start of the day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Time of Day" : {
|
||||
"comment" : "A label for the time of day picker in the ritual edit sheet.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Today" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -2033,6 +2538,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Transition to rest" : {
|
||||
"comment" : "Theme of the \"Evening Wind-Down\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Unlimited Rituals" : {
|
||||
"comment" : "Description of a feature in the \"Pro\" upgrade section, describing that users can create as many rituals as they need.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -2049,6 +2558,10 @@
|
||||
"comment" : "Text for a settings card that allows users to upgrade to the Pro version of the app.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Use breath to reduce stress and increase focus." : {
|
||||
"comment" : "Notes for a \"Breathwork\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Vibrate when completing habits" : {
|
||||
|
||||
},
|
||||
@ -2056,10 +2569,30 @@
|
||||
"comment" : "An accessibility label for the weekly completion chart in the insight detail sheet.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Weekly Reset" : {
|
||||
"comment" : "Name of a ritual preset that users can add to their collection, focusing on preparing for a fresh week.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Wellness" : {
|
||||
"comment" : "The category of the morning ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"What could be better?" : {
|
||||
"comment" : "Habit title for a mindfulness ritual where the user reflects on areas they could improve.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"What this means" : {
|
||||
"comment" : "A label displayed above the explanation text in the insight detail sheet.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"What went well today?" : {
|
||||
"comment" : "Habit title for a mindfulness ritual where the user writes down one positive thing that happened during the day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"When you feel scattered, use this to refocus." : {
|
||||
"comment" : "Title of a ritual preset focused on helping users regain clarity when they feel scattered.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Why arcs keep habits grounded" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -2082,6 +2615,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Wind down with habits that promote quality sleep." : {
|
||||
"comment" : "Notes section of a ritual preset focused on sleep preparation.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Wind down with quiet, consistent cues." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -2104,6 +2641,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Write 3 things you're grateful for" : {
|
||||
"comment" : "Title of a habit within a RitualPreset focused on gratitude practice.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Write down what's on your mind" : {
|
||||
"comment" : "Habit title for a ritual preset that encourages the user to write down their thoughts.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Your active and recent arcs" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
||||
@ -1,6 +1,29 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
/// Represents when a ritual should appear in the Today view.
|
||||
enum TimeOfDay: String, Codable, CaseIterable {
|
||||
case morning // Before noon
|
||||
case evening // After 5pm
|
||||
case anytime // Always visible
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .morning: return String(localized: "Morning")
|
||||
case .evening: return String(localized: "Evening")
|
||||
case .anytime: return String(localized: "Anytime")
|
||||
}
|
||||
}
|
||||
|
||||
var symbolName: String {
|
||||
switch self {
|
||||
case .morning: return "sunrise.fill"
|
||||
case .evening: return "moon.stars.fill"
|
||||
case .anytime: return "clock.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
final class Ritual {
|
||||
var id: UUID
|
||||
@ -11,6 +34,17 @@ final class Ritual {
|
||||
@Relationship(deleteRule: .cascade)
|
||||
var habits: [Habit]
|
||||
var notes: String
|
||||
|
||||
// Management
|
||||
var isEnabled: Bool
|
||||
var isArchived: Bool
|
||||
|
||||
// Scheduling
|
||||
var timeOfDay: TimeOfDay
|
||||
|
||||
// Organization
|
||||
var iconName: String
|
||||
var category: String
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
@ -19,7 +53,12 @@ final class Ritual {
|
||||
startDate: Date = Date(),
|
||||
durationDays: Int = 28,
|
||||
habits: [Habit] = [],
|
||||
notes: String = ""
|
||||
notes: String = "",
|
||||
isEnabled: Bool = true,
|
||||
isArchived: Bool = false,
|
||||
timeOfDay: TimeOfDay = .anytime,
|
||||
iconName: String = "sparkles",
|
||||
category: String = ""
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
@ -28,5 +67,10 @@ final class Ritual {
|
||||
self.durationDays = durationDays
|
||||
self.habits = habits
|
||||
self.notes = notes
|
||||
self.isEnabled = isEnabled
|
||||
self.isArchived = isArchived
|
||||
self.timeOfDay = timeOfDay
|
||||
self.iconName = iconName
|
||||
self.category = category
|
||||
}
|
||||
}
|
||||
|
||||
282
Andromida/App/Models/RitualPresets.swift
Normal file
282
Andromida/App/Models/RitualPresets.swift
Normal file
@ -0,0 +1,282 @@
|
||||
import Foundation
|
||||
|
||||
/// A template for a habit within a preset ritual
|
||||
struct HabitPreset: Identifiable {
|
||||
let id = UUID()
|
||||
let title: String
|
||||
let symbolName: String
|
||||
}
|
||||
|
||||
/// A template ritual that users can add to their collection
|
||||
struct RitualPreset: Identifiable {
|
||||
let id = UUID()
|
||||
let title: String
|
||||
let theme: String
|
||||
let notes: String
|
||||
let durationDays: Int
|
||||
let timeOfDay: TimeOfDay
|
||||
let iconName: String
|
||||
let category: String
|
||||
let habits: [HabitPreset]
|
||||
}
|
||||
|
||||
/// Categories for organizing presets
|
||||
enum PresetCategory: String, CaseIterable {
|
||||
case health = "Health"
|
||||
case productivity = "Productivity"
|
||||
case mindfulness = "Mindfulness"
|
||||
case selfCare = "Self-Care"
|
||||
|
||||
var displayName: String {
|
||||
rawValue
|
||||
}
|
||||
|
||||
var symbolName: String {
|
||||
switch self {
|
||||
case .health: return "heart.fill"
|
||||
case .productivity: return "bolt.fill"
|
||||
case .mindfulness: return "brain.head.profile"
|
||||
case .selfCare: return "sparkles"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Library of preset rituals organized by category
|
||||
enum RitualPresetLibrary {
|
||||
|
||||
static var allPresets: [RitualPreset] {
|
||||
healthPresets + productivityPresets + mindfulnessPresets + selfCarePresets
|
||||
}
|
||||
|
||||
static func presets(for category: PresetCategory) -> [RitualPreset] {
|
||||
switch category {
|
||||
case .health: return healthPresets
|
||||
case .productivity: return productivityPresets
|
||||
case .mindfulness: return mindfulnessPresets
|
||||
case .selfCare: return selfCarePresets
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Health Presets
|
||||
|
||||
static let healthPresets: [RitualPreset] = [
|
||||
RitualPreset(
|
||||
title: String(localized: "Morning Hydration"),
|
||||
theme: String(localized: "Start your day refreshed"),
|
||||
notes: String(localized: "Build the habit of hydrating first thing in the morning."),
|
||||
durationDays: 21,
|
||||
timeOfDay: .morning,
|
||||
iconName: "drop.fill",
|
||||
category: PresetCategory.health.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "Drink a glass of water"), symbolName: "drop.fill"),
|
||||
HabitPreset(title: String(localized: "Take vitamins"), symbolName: "pill.fill"),
|
||||
HabitPreset(title: String(localized: "Eat a healthy breakfast"), symbolName: "fork.knife")
|
||||
]
|
||||
),
|
||||
RitualPreset(
|
||||
title: String(localized: "Midday Movement"),
|
||||
theme: String(localized: "Break up your day"),
|
||||
notes: String(localized: "Combat sedentary habits with midday activity."),
|
||||
durationDays: 28,
|
||||
timeOfDay: .anytime,
|
||||
iconName: "figure.walk",
|
||||
category: PresetCategory.health.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "Take a 10-minute walk"), symbolName: "figure.walk"),
|
||||
HabitPreset(title: String(localized: "Stretch at your desk"), symbolName: "figure.flexibility"),
|
||||
HabitPreset(title: String(localized: "Drink water"), symbolName: "drop.fill")
|
||||
]
|
||||
),
|
||||
RitualPreset(
|
||||
title: String(localized: "Sleep Preparation"),
|
||||
theme: String(localized: "Rest better tonight"),
|
||||
notes: String(localized: "Wind down with habits that promote quality sleep."),
|
||||
durationDays: 28,
|
||||
timeOfDay: .evening,
|
||||
iconName: "moon.zzz.fill",
|
||||
category: PresetCategory.health.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "No caffeine after 2pm"), symbolName: "cup.and.saucer.fill"),
|
||||
HabitPreset(title: String(localized: "Dim lights 1 hour before bed"), symbolName: "lightbulb.fill"),
|
||||
HabitPreset(title: String(localized: "Set consistent bedtime"), symbolName: "bed.double.fill"),
|
||||
HabitPreset(title: String(localized: "Keep room cool"), symbolName: "thermometer.snowflake")
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
// MARK: - Productivity Presets
|
||||
|
||||
static let productivityPresets: [RitualPreset] = [
|
||||
RitualPreset(
|
||||
title: String(localized: "Deep Work Prep"),
|
||||
theme: String(localized: "Set up for focus"),
|
||||
notes: String(localized: "Create the conditions for uninterrupted deep work."),
|
||||
durationDays: 21,
|
||||
timeOfDay: .morning,
|
||||
iconName: "brain",
|
||||
category: PresetCategory.productivity.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "Clear your desk"), symbolName: "tray.full.fill"),
|
||||
HabitPreset(title: String(localized: "Set phone to Do Not Disturb"), symbolName: "bell.slash.fill"),
|
||||
HabitPreset(title: String(localized: "Define your top 3 priorities"), symbolName: "list.number"),
|
||||
HabitPreset(title: String(localized: "Close unnecessary tabs"), symbolName: "xmark.square.fill")
|
||||
]
|
||||
),
|
||||
RitualPreset(
|
||||
title: String(localized: "End-of-Day Review"),
|
||||
theme: String(localized: "Close loops, plan ahead"),
|
||||
notes: String(localized: "Review your day and set up tomorrow for success."),
|
||||
durationDays: 28,
|
||||
timeOfDay: .evening,
|
||||
iconName: "checkmark.circle.fill",
|
||||
category: PresetCategory.productivity.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "Review completed tasks"), symbolName: "checkmark.square.fill"),
|
||||
HabitPreset(title: String(localized: "Clear your inbox"), symbolName: "envelope.fill"),
|
||||
HabitPreset(title: String(localized: "Plan tomorrow's priorities"), symbolName: "calendar.badge.plus"),
|
||||
HabitPreset(title: String(localized: "Tidy workspace"), symbolName: "sparkles")
|
||||
]
|
||||
),
|
||||
RitualPreset(
|
||||
title: String(localized: "Focus Reset"),
|
||||
theme: String(localized: "Regain clarity"),
|
||||
notes: String(localized: "When you feel scattered, use this to refocus."),
|
||||
durationDays: 14,
|
||||
timeOfDay: .anytime,
|
||||
iconName: "target",
|
||||
category: PresetCategory.productivity.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "Take 5 deep breaths"), symbolName: "wind"),
|
||||
HabitPreset(title: String(localized: "Write down what's on your mind"), symbolName: "pencil.and.list.clipboard"),
|
||||
HabitPreset(title: String(localized: "Choose ONE thing to focus on"), symbolName: "scope")
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
// MARK: - Mindfulness Presets
|
||||
|
||||
static let mindfulnessPresets: [RitualPreset] = [
|
||||
RitualPreset(
|
||||
title: String(localized: "Morning Meditation"),
|
||||
theme: String(localized: "Start with stillness"),
|
||||
notes: String(localized: "A calm mind sets the tone for a calm day."),
|
||||
durationDays: 30,
|
||||
timeOfDay: .morning,
|
||||
iconName: "figure.mind.and.body",
|
||||
category: PresetCategory.mindfulness.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "5-minute meditation"), symbolName: "figure.mind.and.body"),
|
||||
HabitPreset(title: String(localized: "Set an intention for the day"), symbolName: "star.fill"),
|
||||
HabitPreset(title: String(localized: "Practice gratitude"), symbolName: "heart.fill")
|
||||
]
|
||||
),
|
||||
RitualPreset(
|
||||
title: String(localized: "Gratitude Practice"),
|
||||
theme: String(localized: "Find the good"),
|
||||
notes: String(localized: "Shift your focus to what's going well."),
|
||||
durationDays: 21,
|
||||
timeOfDay: .evening,
|
||||
iconName: "heart.text.square.fill",
|
||||
category: PresetCategory.mindfulness.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "Write 3 things you're grateful for"), symbolName: "list.bullet.clipboard.fill"),
|
||||
HabitPreset(title: String(localized: "Thank someone"), symbolName: "person.wave.2.fill"),
|
||||
HabitPreset(title: String(localized: "Reflect on a positive moment"), symbolName: "sun.max.fill")
|
||||
]
|
||||
),
|
||||
RitualPreset(
|
||||
title: String(localized: "Breathwork"),
|
||||
theme: String(localized: "Calm your nervous system"),
|
||||
notes: String(localized: "Use breath to reduce stress and increase focus."),
|
||||
durationDays: 14,
|
||||
timeOfDay: .anytime,
|
||||
iconName: "wind",
|
||||
category: PresetCategory.mindfulness.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "Box breathing (4-4-4-4)"), symbolName: "square"),
|
||||
HabitPreset(title: String(localized: "Body scan for tension"), symbolName: "figure.stand"),
|
||||
HabitPreset(title: String(localized: "Release shoulder tension"), symbolName: "arrow.down.circle.fill")
|
||||
]
|
||||
),
|
||||
RitualPreset(
|
||||
title: String(localized: "Evening Reflection"),
|
||||
theme: String(localized: "Process your day"),
|
||||
notes: String(localized: "Close the day with awareness and intention."),
|
||||
durationDays: 28,
|
||||
timeOfDay: .evening,
|
||||
iconName: "moon.stars.fill",
|
||||
category: PresetCategory.mindfulness.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "Journal for 5 minutes"), symbolName: "book.fill"),
|
||||
HabitPreset(title: String(localized: "What went well today?"), symbolName: "hand.thumbsup.fill"),
|
||||
HabitPreset(title: String(localized: "What could be better?"), symbolName: "lightbulb.fill"),
|
||||
HabitPreset(title: String(localized: "Let go of the day"), symbolName: "leaf.fill")
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
// MARK: - Self-Care Presets
|
||||
|
||||
static let selfCarePresets: [RitualPreset] = [
|
||||
RitualPreset(
|
||||
title: String(localized: "Morning Skincare"),
|
||||
theme: String(localized: "Care for yourself"),
|
||||
notes: String(localized: "A consistent skincare routine for healthy skin."),
|
||||
durationDays: 28,
|
||||
timeOfDay: .morning,
|
||||
iconName: "drop.triangle.fill",
|
||||
category: PresetCategory.selfCare.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "Cleanse face"), symbolName: "drop.fill"),
|
||||
HabitPreset(title: String(localized: "Apply moisturizer"), symbolName: "humidity.fill"),
|
||||
HabitPreset(title: String(localized: "Apply sunscreen"), symbolName: "sun.max.fill")
|
||||
]
|
||||
),
|
||||
RitualPreset(
|
||||
title: String(localized: "Digital Detox"),
|
||||
theme: String(localized: "Disconnect to reconnect"),
|
||||
notes: String(localized: "Give your mind a break from screens."),
|
||||
durationDays: 21,
|
||||
timeOfDay: .evening,
|
||||
iconName: "iphone.slash",
|
||||
category: PresetCategory.selfCare.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "No screens 1 hour before bed"), symbolName: "iphone.slash"),
|
||||
HabitPreset(title: String(localized: "Read a physical book"), symbolName: "book.closed.fill"),
|
||||
HabitPreset(title: String(localized: "Have a real conversation"), symbolName: "person.2.fill")
|
||||
]
|
||||
),
|
||||
RitualPreset(
|
||||
title: String(localized: "Evening Wind-Down"),
|
||||
theme: String(localized: "Transition to rest"),
|
||||
notes: String(localized: "Create a buffer between your day and sleep."),
|
||||
durationDays: 28,
|
||||
timeOfDay: .evening,
|
||||
iconName: "moon.fill",
|
||||
category: PresetCategory.selfCare.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "Change into comfortable clothes"), symbolName: "tshirt.fill"),
|
||||
HabitPreset(title: String(localized: "Light a candle or dim lights"), symbolName: "flame.fill"),
|
||||
HabitPreset(title: String(localized: "Gentle stretching"), symbolName: "figure.flexibility"),
|
||||
HabitPreset(title: String(localized: "Herbal tea"), symbolName: "cup.and.saucer.fill")
|
||||
]
|
||||
),
|
||||
RitualPreset(
|
||||
title: String(localized: "Weekly Reset"),
|
||||
theme: String(localized: "Prepare for a fresh week"),
|
||||
notes: String(localized: "Sunday evening ritual to start Monday strong."),
|
||||
durationDays: 12,
|
||||
timeOfDay: .evening,
|
||||
iconName: "arrow.counterclockwise.circle.fill",
|
||||
category: PresetCategory.selfCare.rawValue,
|
||||
habits: [
|
||||
HabitPreset(title: String(localized: "Review last week"), symbolName: "calendar"),
|
||||
HabitPreset(title: String(localized: "Plan the week ahead"), symbolName: "calendar.badge.plus"),
|
||||
HabitPreset(title: String(localized: "Prepare clothes for Monday"), symbolName: "tshirt.fill"),
|
||||
HabitPreset(title: String(localized: "Tidy your space"), symbolName: "sparkles"),
|
||||
HabitPreset(title: String(localized: "Early bedtime"), symbolName: "bed.double.fill")
|
||||
]
|
||||
)
|
||||
]
|
||||
}
|
||||
@ -19,7 +19,10 @@ struct RitualSeedService: RitualSeedProviding {
|
||||
startDate: startDate,
|
||||
durationDays: 28,
|
||||
habits: morningHabits,
|
||||
notes: String(localized: "A gentle 4-week arc for energy and focus.")
|
||||
notes: String(localized: "A gentle 4-week arc for energy and focus."),
|
||||
timeOfDay: .morning,
|
||||
iconName: "sunrise.fill",
|
||||
category: String(localized: "Wellness")
|
||||
)
|
||||
|
||||
let eveningRitual = Ritual(
|
||||
@ -28,7 +31,10 @@ struct RitualSeedService: RitualSeedProviding {
|
||||
startDate: Calendar.current.date(byAdding: .day, value: -14, to: startDate) ?? startDate,
|
||||
durationDays: 28,
|
||||
habits: eveningHabits,
|
||||
notes: String(localized: "Wind down with quiet, consistent cues.")
|
||||
notes: String(localized: "Wind down with quiet, consistent cues."),
|
||||
timeOfDay: .evening,
|
||||
iconName: "moon.stars.fill",
|
||||
category: String(localized: "Wellness")
|
||||
)
|
||||
|
||||
return [morningRitual, eveningRitual]
|
||||
|
||||
@ -340,6 +340,130 @@ final class RitualStore: RitualStoreProviding {
|
||||
modelContext.insert(ritual)
|
||||
saveContext()
|
||||
}
|
||||
|
||||
// MARK: - Ritual Management
|
||||
|
||||
/// Rituals that are enabled and not archived (visible in Today/Rituals views)
|
||||
var enabledRituals: [Ritual] {
|
||||
rituals.filter { $0.isEnabled && !$0.isArchived }
|
||||
}
|
||||
|
||||
/// Rituals that have been archived
|
||||
var archivedRituals: [Ritual] {
|
||||
rituals.filter { $0.isArchived }
|
||||
}
|
||||
|
||||
/// Returns rituals appropriate for the current time of day
|
||||
func ritualsForCurrentTime() -> [Ritual] {
|
||||
let hour = calendar.component(.hour, from: Date())
|
||||
let currentPeriod: TimeOfDay = hour < 12 ? .morning : .evening
|
||||
|
||||
return enabledRituals.filter { ritual in
|
||||
ritual.timeOfDay == .anytime || ritual.timeOfDay == currentPeriod
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new ritual with the given properties
|
||||
func createRitual(
|
||||
title: String,
|
||||
theme: String,
|
||||
notes: String = "",
|
||||
durationDays: Int = 28,
|
||||
timeOfDay: TimeOfDay = .anytime,
|
||||
iconName: String = "sparkles",
|
||||
category: String = "",
|
||||
habits: [Habit] = []
|
||||
) {
|
||||
let ritual = Ritual(
|
||||
title: title,
|
||||
theme: theme,
|
||||
startDate: Date(),
|
||||
durationDays: durationDays,
|
||||
habits: habits,
|
||||
notes: notes,
|
||||
timeOfDay: timeOfDay,
|
||||
iconName: iconName,
|
||||
category: category
|
||||
)
|
||||
modelContext.insert(ritual)
|
||||
saveContext()
|
||||
}
|
||||
|
||||
/// Creates a ritual from a preset template
|
||||
func createRitualFromPreset(_ preset: RitualPreset) {
|
||||
let habits = preset.habits.map { habitPreset in
|
||||
Habit(title: habitPreset.title, symbolName: habitPreset.symbolName)
|
||||
}
|
||||
createRitual(
|
||||
title: preset.title,
|
||||
theme: preset.theme,
|
||||
notes: preset.notes,
|
||||
durationDays: preset.durationDays,
|
||||
timeOfDay: preset.timeOfDay,
|
||||
iconName: preset.iconName,
|
||||
category: preset.category,
|
||||
habits: habits
|
||||
)
|
||||
}
|
||||
|
||||
/// Updates an existing ritual's properties
|
||||
func updateRitual(
|
||||
_ ritual: Ritual,
|
||||
title: String,
|
||||
theme: String,
|
||||
notes: String,
|
||||
durationDays: Int,
|
||||
timeOfDay: TimeOfDay,
|
||||
iconName: String,
|
||||
category: String
|
||||
) {
|
||||
ritual.title = title
|
||||
ritual.theme = theme
|
||||
ritual.notes = notes
|
||||
ritual.durationDays = durationDays
|
||||
ritual.timeOfDay = timeOfDay
|
||||
ritual.iconName = iconName
|
||||
ritual.category = category
|
||||
saveContext()
|
||||
}
|
||||
|
||||
/// Permanently deletes a ritual and all its history
|
||||
func deleteRitual(_ ritual: Ritual) {
|
||||
modelContext.delete(ritual)
|
||||
saveContext()
|
||||
}
|
||||
|
||||
/// Toggles whether a ritual is enabled (shows in Today view)
|
||||
func toggleEnabled(_ ritual: Ritual) {
|
||||
ritual.isEnabled.toggle()
|
||||
saveContext()
|
||||
}
|
||||
|
||||
/// Archives a ritual (hides it but preserves history)
|
||||
func archiveRitual(_ ritual: Ritual) {
|
||||
ritual.isArchived = true
|
||||
saveContext()
|
||||
}
|
||||
|
||||
/// Unarchives a ritual (restores it to active list)
|
||||
func unarchiveRitual(_ ritual: Ritual) {
|
||||
ritual.isArchived = false
|
||||
saveContext()
|
||||
}
|
||||
|
||||
/// Adds a habit to an existing ritual
|
||||
func addHabit(to ritual: Ritual, title: String, symbolName: String) {
|
||||
let habit = Habit(title: title, symbolName: symbolName)
|
||||
ritual.habits.append(habit)
|
||||
saveContext()
|
||||
}
|
||||
|
||||
/// Removes a habit from a ritual
|
||||
func removeHabit(_ habit: Habit, from ritual: Ritual) {
|
||||
ritual.habits.removeAll { $0.id == habit.id }
|
||||
modelContext.delete(habit)
|
||||
saveContext()
|
||||
}
|
||||
|
||||
private func loadRitualsIfNeeded() {
|
||||
reloadRituals()
|
||||
|
||||
@ -6,36 +6,63 @@ struct RitualCardView: View {
|
||||
private let theme: String
|
||||
private let dayLabel: String
|
||||
private let completionSummary: String
|
||||
private let iconName: String
|
||||
private let timeOfDay: TimeOfDay
|
||||
private let isEnabled: Bool
|
||||
|
||||
init(
|
||||
title: String,
|
||||
theme: String,
|
||||
dayLabel: String,
|
||||
completionSummary: String
|
||||
completionSummary: String,
|
||||
iconName: String = "sparkles",
|
||||
timeOfDay: TimeOfDay = .anytime,
|
||||
isEnabled: Bool = true
|
||||
) {
|
||||
self.title = title
|
||||
self.theme = theme
|
||||
self.dayLabel = dayLabel
|
||||
self.completionSummary = completionSummary
|
||||
self.iconName = iconName
|
||||
self.timeOfDay = timeOfDay
|
||||
self.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
Image(systemName: "circle.hexagonpath.fill")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
Image(systemName: iconName)
|
||||
.foregroundStyle(isEnabled ? AppAccent.primary : AppTextColors.tertiary)
|
||||
.accessibilityHidden(true)
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
.foregroundStyle(isEnabled ? AppTextColors.primary : AppTextColors.tertiary)
|
||||
|
||||
if !isEnabled {
|
||||
Text(String(localized: "Disabled"))
|
||||
.font(.caption2)
|
||||
.padding(.horizontal, Design.Spacing.small)
|
||||
.padding(.vertical, Design.Spacing.xSmall)
|
||||
.background(AppSurface.secondary)
|
||||
.clipShape(.capsule)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
}
|
||||
|
||||
Spacer(minLength: Design.Spacing.medium)
|
||||
|
||||
// Time of day badge
|
||||
Image(systemName: timeOfDay.symbolName)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
.accessibilityLabel(timeOfDay.displayName)
|
||||
|
||||
Text(dayLabel)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
Text(theme)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.foregroundStyle(isEnabled ? AppTextColors.secondary : AppTextColors.tertiary)
|
||||
Text(completionSummary)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
@ -43,17 +70,33 @@ struct RitualCardView: View {
|
||||
.padding(Design.Spacing.large)
|
||||
.background(AppSurface.card)
|
||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
||||
.opacity(isEnabled ? 1.0 : Design.Opacity.medium)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
RitualCardView(
|
||||
title: "Morning Clarity",
|
||||
theme: "Fresh starts",
|
||||
dayLabel: "Day 6 of 28",
|
||||
completionSummary: "2 of 3 habits complete"
|
||||
)
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
RitualCardView(
|
||||
title: "Morning Clarity",
|
||||
theme: "Fresh starts",
|
||||
dayLabel: "Day 6 of 28",
|
||||
completionSummary: "2 of 3 habits complete",
|
||||
iconName: "sunrise.fill",
|
||||
timeOfDay: .morning,
|
||||
isEnabled: true
|
||||
)
|
||||
|
||||
RitualCardView(
|
||||
title: "Evening Reset",
|
||||
theme: "Soft landings",
|
||||
dayLabel: "Day 14 of 28",
|
||||
completionSummary: "0 of 3 habits complete",
|
||||
iconName: "moon.stars.fill",
|
||||
timeOfDay: .evening,
|
||||
isEnabled: false
|
||||
)
|
||||
}
|
||||
.padding(Design.Spacing.large)
|
||||
.background(AppSurface.primary)
|
||||
}
|
||||
|
||||
@ -3,7 +3,13 @@ import Bedrock
|
||||
|
||||
struct RitualDetailView: View {
|
||||
@Bindable var store: RitualStore
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
private let ritual: Ritual
|
||||
|
||||
@State private var showingEditSheet = false
|
||||
@State private var showingDeleteConfirmation = false
|
||||
@State private var showingArchiveConfirmation = false
|
||||
|
||||
init(store: RitualStore, ritual: Ritual) {
|
||||
self.store = store
|
||||
@ -13,17 +19,10 @@ struct RitualDetailView: View {
|
||||
var body: some View {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.large) {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
||||
Text(ritual.title)
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
.bold()
|
||||
Text(ritual.theme)
|
||||
.font(.title3)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
|
||||
// Header with icon
|
||||
headerSection
|
||||
|
||||
// Progress card
|
||||
RitualFocusCardView(
|
||||
title: ritual.title,
|
||||
theme: ritual.theme,
|
||||
@ -31,7 +30,11 @@ struct RitualDetailView: View {
|
||||
completionSummary: store.completionSummary(for: ritual),
|
||||
progress: store.ritualProgress(for: ritual)
|
||||
)
|
||||
|
||||
// Status badges
|
||||
statusBadges
|
||||
|
||||
// Notes
|
||||
if !ritual.notes.isEmpty {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
||||
SectionHeaderView(title: String(localized: "Notes"))
|
||||
@ -41,6 +44,7 @@ struct RitualDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Habits
|
||||
SectionHeaderView(
|
||||
title: String(localized: "Habits"),
|
||||
subtitle: String(localized: "Tap to check in")
|
||||
@ -66,6 +70,129 @@ struct RitualDetailView: View {
|
||||
))
|
||||
.navigationTitle(String(localized: "Ritual"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Menu {
|
||||
Button {
|
||||
showingEditSheet = true
|
||||
} label: {
|
||||
Label(String(localized: "Edit"), systemImage: "pencil")
|
||||
}
|
||||
|
||||
Button {
|
||||
store.toggleEnabled(ritual)
|
||||
} label: {
|
||||
Label(
|
||||
ritual.isEnabled ? String(localized: "Disable") : String(localized: "Enable"),
|
||||
systemImage: ritual.isEnabled ? "pause.circle" : "play.circle"
|
||||
)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button {
|
||||
showingArchiveConfirmation = true
|
||||
} label: {
|
||||
Label(String(localized: "Archive"), systemImage: "archivebox")
|
||||
}
|
||||
|
||||
Button(role: .destructive) {
|
||||
showingDeleteConfirmation = true
|
||||
} label: {
|
||||
Label(String(localized: "Delete"), systemImage: "trash")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingEditSheet) {
|
||||
RitualEditSheet(store: store, ritual: ritual)
|
||||
}
|
||||
.alert(String(localized: "Delete Ritual?"), isPresented: $showingDeleteConfirmation) {
|
||||
Button(String(localized: "Cancel"), role: .cancel) {}
|
||||
Button(String(localized: "Delete"), role: .destructive) {
|
||||
store.deleteRitual(ritual)
|
||||
dismiss()
|
||||
}
|
||||
} message: {
|
||||
Text(String(localized: "This will permanently remove \"\(ritual.title)\" and all its completion history. This cannot be undone."))
|
||||
}
|
||||
.alert(String(localized: "Archive Ritual?"), isPresented: $showingArchiveConfirmation) {
|
||||
Button(String(localized: "Cancel"), role: .cancel) {}
|
||||
Button(String(localized: "Archive")) {
|
||||
store.archiveRitual(ritual)
|
||||
dismiss()
|
||||
}
|
||||
} message: {
|
||||
Text(String(localized: "This ritual will be hidden from your active list but its history will be preserved."))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Header Section
|
||||
|
||||
private var headerSection: some View {
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
Image(systemName: ritual.iconName)
|
||||
.font(.system(size: Design.BaseFontSize.largeTitle))
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 56, height: 56)
|
||||
.background(AppAccent.primary.opacity(0.1))
|
||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
||||
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
||||
Text(ritual.title)
|
||||
.font(.title2)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
.bold()
|
||||
Text(ritual.theme)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
// MARK: - Status Badges
|
||||
|
||||
private var statusBadges: some View {
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
// Time of day badge
|
||||
Label(ritual.timeOfDay.displayName, systemImage: ritual.timeOfDay.symbolName)
|
||||
.font(.caption)
|
||||
.padding(.horizontal, Design.Spacing.small)
|
||||
.padding(.vertical, Design.Spacing.xSmall)
|
||||
.background(AppSurface.card)
|
||||
.clipShape(.capsule)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
|
||||
// Category badge (if set)
|
||||
if !ritual.category.isEmpty {
|
||||
Text(ritual.category)
|
||||
.font(.caption)
|
||||
.padding(.horizontal, Design.Spacing.small)
|
||||
.padding(.vertical, Design.Spacing.xSmall)
|
||||
.background(AppSurface.card)
|
||||
.clipShape(.capsule)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
|
||||
// Enabled/Disabled badge
|
||||
if !ritual.isEnabled {
|
||||
Text(String(localized: "Disabled"))
|
||||
.font(.caption)
|
||||
.padding(.horizontal, Design.Spacing.small)
|
||||
.padding(.vertical, Design.Spacing.xSmall)
|
||||
.background(AppStatus.warning.opacity(0.2))
|
||||
.clipShape(.capsule)
|
||||
.foregroundStyle(AppStatus.warning)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,10 @@ import Bedrock
|
||||
|
||||
struct RitualsView: View {
|
||||
@Bindable var store: RitualStore
|
||||
@State private var showingPresetLibrary = false
|
||||
@State private var showingCreateRitual = false
|
||||
@State private var ritualToDelete: Ritual?
|
||||
@State private var ritualToArchive: Ritual?
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
@ -12,20 +16,25 @@ struct RitualsView: View {
|
||||
subtitle: String(localized: "Your active and recent arcs")
|
||||
)
|
||||
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
ForEach(store.rituals) { ritual in
|
||||
NavigationLink {
|
||||
RitualDetailView(store: store, ritual: ritual)
|
||||
} label: {
|
||||
RitualCardView(
|
||||
title: ritual.title,
|
||||
theme: ritual.theme,
|
||||
dayLabel: store.ritualDayLabel(for: ritual),
|
||||
completionSummary: store.completionSummary(for: ritual)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
// Active rituals
|
||||
if !store.enabledRituals.isEmpty {
|
||||
activeRitualsSection
|
||||
}
|
||||
|
||||
// Disabled rituals
|
||||
let disabledRituals = store.rituals.filter { !$0.isEnabled && !$0.isArchived }
|
||||
if !disabledRituals.isEmpty {
|
||||
disabledRitualsSection(disabledRituals)
|
||||
}
|
||||
|
||||
// Archived rituals
|
||||
if !store.archivedRituals.isEmpty {
|
||||
archivedRitualsSection
|
||||
}
|
||||
|
||||
// Empty state
|
||||
if store.rituals.isEmpty {
|
||||
emptyState
|
||||
}
|
||||
}
|
||||
.padding(Design.Spacing.large)
|
||||
@ -35,6 +44,247 @@ struct RitualsView: View {
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
))
|
||||
.navigationTitle(String(localized: "Rituals"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Menu {
|
||||
Button {
|
||||
showingCreateRitual = true
|
||||
} label: {
|
||||
Label(String(localized: "Create New"), systemImage: "plus.circle")
|
||||
}
|
||||
|
||||
Button {
|
||||
showingPresetLibrary = true
|
||||
} label: {
|
||||
Label(String(localized: "Browse Presets"), systemImage: "sparkles.rectangle.stack")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingPresetLibrary) {
|
||||
PresetLibrarySheet(store: store)
|
||||
}
|
||||
.sheet(isPresented: $showingCreateRitual) {
|
||||
RitualEditSheet(store: store, ritual: nil)
|
||||
}
|
||||
.alert(String(localized: "Delete Ritual?"), isPresented: .init(
|
||||
get: { ritualToDelete != nil },
|
||||
set: { if !$0 { ritualToDelete = nil } }
|
||||
)) {
|
||||
Button(String(localized: "Cancel"), role: .cancel) {
|
||||
ritualToDelete = nil
|
||||
}
|
||||
Button(String(localized: "Delete"), role: .destructive) {
|
||||
if let ritual = ritualToDelete {
|
||||
store.deleteRitual(ritual)
|
||||
}
|
||||
ritualToDelete = nil
|
||||
}
|
||||
} message: {
|
||||
Text(String(localized: "This will permanently remove this ritual and all its completion history. This cannot be undone."))
|
||||
}
|
||||
.alert(String(localized: "Archive Ritual?"), isPresented: .init(
|
||||
get: { ritualToArchive != nil },
|
||||
set: { if !$0 { ritualToArchive = nil } }
|
||||
)) {
|
||||
Button(String(localized: "Cancel"), role: .cancel) {
|
||||
ritualToArchive = nil
|
||||
}
|
||||
Button(String(localized: "Archive")) {
|
||||
if let ritual = ritualToArchive {
|
||||
store.archiveRitual(ritual)
|
||||
}
|
||||
ritualToArchive = nil
|
||||
}
|
||||
} message: {
|
||||
Text(String(localized: "This ritual will be hidden from your active list but its history will be preserved."))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sections
|
||||
|
||||
private var activeRitualsSection: some View {
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
ForEach(store.enabledRituals) { ritual in
|
||||
ritualRow(for: ritual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func disabledRitualsSection(_ rituals: [Ritual]) -> some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||
Text(String(localized: "Disabled"))
|
||||
.font(.subheadline)
|
||||
.bold()
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.padding(.top, Design.Spacing.medium)
|
||||
|
||||
ForEach(rituals) { ritual in
|
||||
ritualRow(for: ritual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var archivedRitualsSection: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||
Text(String(localized: "Archived"))
|
||||
.font(.subheadline)
|
||||
.bold()
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.padding(.top, Design.Spacing.medium)
|
||||
|
||||
ForEach(store.archivedRituals) { ritual in
|
||||
archivedRitualRow(for: ritual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyState: some View {
|
||||
VStack(spacing: Design.Spacing.large) {
|
||||
Image(systemName: "sparkles")
|
||||
.font(.system(size: Design.BaseFontSize.largeTitle * 2))
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
|
||||
Text(String(localized: "No Rituals Yet"))
|
||||
.font(.headline)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
|
||||
Text(String(localized: "Create a custom ritual or browse our preset library to get started."))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
Button {
|
||||
showingCreateRitual = true
|
||||
} label: {
|
||||
Label(String(localized: "Create"), systemImage: "plus")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(AppAccent.primary)
|
||||
|
||||
Button {
|
||||
showingPresetLibrary = true
|
||||
} label: {
|
||||
Label(String(localized: "Presets"), systemImage: "sparkles.rectangle.stack")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, Design.Spacing.xxxLarge)
|
||||
}
|
||||
|
||||
// MARK: - Ritual Rows
|
||||
|
||||
private func ritualRow(for ritual: Ritual) -> some View {
|
||||
NavigationLink {
|
||||
RitualDetailView(store: store, ritual: ritual)
|
||||
} label: {
|
||||
RitualCardView(
|
||||
title: ritual.title,
|
||||
theme: ritual.theme,
|
||||
dayLabel: store.ritualDayLabel(for: ritual),
|
||||
completionSummary: store.completionSummary(for: ritual),
|
||||
iconName: ritual.iconName,
|
||||
timeOfDay: ritual.timeOfDay,
|
||||
isEnabled: ritual.isEnabled
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contextMenu {
|
||||
contextMenuItems(for: ritual)
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button(role: .destructive) {
|
||||
ritualToDelete = ritual
|
||||
} label: {
|
||||
Label(String(localized: "Delete"), systemImage: "trash")
|
||||
}
|
||||
|
||||
Button {
|
||||
ritualToArchive = ritual
|
||||
} label: {
|
||||
Label(String(localized: "Archive"), systemImage: "archivebox")
|
||||
}
|
||||
.tint(AppAccent.secondary)
|
||||
}
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
Button {
|
||||
store.toggleEnabled(ritual)
|
||||
} label: {
|
||||
Label(
|
||||
ritual.isEnabled ? String(localized: "Disable") : String(localized: "Enable"),
|
||||
systemImage: ritual.isEnabled ? "pause.circle" : "play.circle"
|
||||
)
|
||||
}
|
||||
.tint(ritual.isEnabled ? AppTextColors.tertiary : AppStatus.success)
|
||||
}
|
||||
}
|
||||
|
||||
private func archivedRitualRow(for ritual: Ritual) -> some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
||||
Text(ritual.title)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
Text(ritual.theme)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
store.unarchiveRitual(ritual)
|
||||
} label: {
|
||||
Text(String(localized: "Restore"))
|
||||
.font(.caption)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
.padding(Design.Spacing.medium)
|
||||
.background(AppSurface.card)
|
||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
|
||||
.opacity(Design.Opacity.medium)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button(role: .destructive) {
|
||||
ritualToDelete = ritual
|
||||
} label: {
|
||||
Label(String(localized: "Delete"), systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func contextMenuItems(for ritual: Ritual) -> some View {
|
||||
Button {
|
||||
store.toggleEnabled(ritual)
|
||||
} label: {
|
||||
Label(
|
||||
ritual.isEnabled ? String(localized: "Disable") : String(localized: "Enable"),
|
||||
systemImage: ritual.isEnabled ? "pause.circle" : "play.circle"
|
||||
)
|
||||
}
|
||||
|
||||
Button {
|
||||
ritualToArchive = ritual
|
||||
} label: {
|
||||
Label(String(localized: "Archive"), systemImage: "archivebox")
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button(role: .destructive) {
|
||||
ritualToDelete = ritual
|
||||
} label: {
|
||||
Label(String(localized: "Delete"), systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
299
Andromida/App/Views/Rituals/Sheets/PresetLibrarySheet.swift
Normal file
299
Andromida/App/Views/Rituals/Sheets/PresetLibrarySheet.swift
Normal file
@ -0,0 +1,299 @@
|
||||
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([.medium, .large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
|
||||
private var headerSection: some View {
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
Image(systemName: preset.iconName)
|
||||
.font(.system(size: Design.BaseFontSize.largeTitle * 2))
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
|
||||
Text(preset.theme)
|
||||
.font(.title3)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
|
||||
HStack(spacing: Design.Spacing.large) {
|
||||
Label(preset.timeOfDay.displayName, systemImage: preset.timeOfDay.symbolName)
|
||||
Label(String(localized: "\(preset.durationDays) days"), systemImage: "calendar")
|
||||
}
|
||||
.font(.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)
|
||||
}
|
||||
370
Andromida/App/Views/Rituals/Sheets/RitualEditSheet.swift
Normal file
370
Andromida/App/Views/Rituals/Sheets/RitualEditSheet.swift
Normal file
@ -0,0 +1,370 @@
|
||||
import SwiftUI
|
||||
import Bedrock
|
||||
|
||||
/// Sheet for creating or editing a ritual
|
||||
struct RitualEditSheet: View {
|
||||
@Bindable var store: RitualStore
|
||||
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 category: String = ""
|
||||
@State private var habits: [EditableHabit] = []
|
||||
|
||||
@State private var showingIconPicker = false
|
||||
@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, ritual: Ritual?) {
|
||||
self.store = store
|
||||
self.ritual = ritual
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
// Basic info section
|
||||
basicInfoSection
|
||||
|
||||
// Schedule section
|
||||
scheduleSection
|
||||
|
||||
// Habits section
|
||||
habitsSection
|
||||
|
||||
// Notes section
|
||||
notesSection
|
||||
}
|
||||
.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)
|
||||
}
|
||||
}
|
||||
.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)
|
||||
|
||||
Picker(String(localized: "Category"), selection: $category) {
|
||||
Text(String(localized: "None")).tag("")
|
||||
ForEach(PresetCategory.allCases, id: \.self) { cat in
|
||||
Text(cat.displayName).tag(cat.rawValue)
|
||||
}
|
||||
}
|
||||
.listRowBackground(AppSurface.card)
|
||||
} header: {
|
||||
Text(String(localized: "Details"))
|
||||
}
|
||||
}
|
||||
|
||||
private var scheduleSection: some View {
|
||||
Section {
|
||||
Picker(String(localized: "Time of Day"), selection: $timeOfDay) {
|
||||
ForEach(TimeOfDay.allCases, id: \.self) { time in
|
||||
Label(time.displayName, systemImage: time.symbolName)
|
||||
.tag(time)
|
||||
}
|
||||
}
|
||||
.listRowBackground(AppSurface.card)
|
||||
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
HStack {
|
||||
Text(String(localized: "Duration"))
|
||||
Spacer()
|
||||
Text(String(localized: "\(Int(durationDays)) days"))
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
|
||||
Slider(value: $durationDays, in: 7...90, step: 1)
|
||||
.tint(AppAccent.primary)
|
||||
}
|
||||
.listRowBackground(AppSurface.card)
|
||||
} header: {
|
||||
Text(String(localized: "Schedule"))
|
||||
}
|
||||
}
|
||||
|
||||
private var habitsSection: some View {
|
||||
Section {
|
||||
ForEach($habits) { $habit in
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
Image(systemName: habit.symbolName)
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
|
||||
TextField(String(localized: "Habit name"), text: $habit.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 {
|
||||
// Simple icon rotation for new habits
|
||||
let icons = ["circle.fill", "star.fill", "heart.fill", "bolt.fill", "leaf.fill"]
|
||||
newHabitIcon = icons.randomElement() ?? "circle.fill"
|
||||
} label: {
|
||||
Image(systemName: newHabitIcon)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
.frame(width: 24)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
category = ritual.category
|
||||
habits = ritual.habits.map { EditableHabit(from: $0) }
|
||||
}
|
||||
|
||||
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 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: category
|
||||
)
|
||||
|
||||
// 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.map { Habit(title: $0.title, symbolName: $0.symbolName) }
|
||||
store.createRitual(
|
||||
title: trimmedTitle,
|
||||
theme: trimmedTheme,
|
||||
notes: trimmedNotes,
|
||||
durationDays: Int(durationDays),
|
||||
timeOfDay: timeOfDay,
|
||||
iconName: iconName,
|
||||
category: category,
|
||||
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: Habit) {
|
||||
self.title = habit.title
|
||||
self.symbolName = habit.symbolName
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple icon picker sheet
|
||||
struct IconPickerSheet: View {
|
||||
@Binding var selectedIcon: String
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
private let icons = [
|
||||
// Wellness
|
||||
"heart.fill", "drop.fill", "leaf.fill", "flame.fill", "bolt.fill",
|
||||
// Time
|
||||
"sunrise.fill", "sun.max.fill", "moon.stars.fill", "clock.fill",
|
||||
// Activity
|
||||
"figure.walk", "figure.run", "figure.mind.and.body", "figure.flexibility",
|
||||
// Mind
|
||||
"brain", "brain.head.profile", "sparkles", "star.fill",
|
||||
// Objects
|
||||
"book.fill", "pencil.and.list.clipboard", "cup.and.saucer.fill", "bed.double.fill",
|
||||
// Misc
|
||||
"checkmark.circle.fill", "target", "scope", "wind"
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 5), spacing: Design.Spacing.medium) {
|
||||
ForEach(icons, id: \.self) { icon in
|
||||
Button {
|
||||
selectedIcon = icon
|
||||
dismiss()
|
||||
} label: {
|
||||
Image(systemName: icon)
|
||||
.font(.title2)
|
||||
.foregroundStyle(selectedIcon == icon ? AppAccent.primary : AppTextColors.secondary)
|
||||
.frame(width: 50, height: 50)
|
||||
.background(selectedIcon == icon ? AppAccent.primary.opacity(0.2) : AppSurface.card)
|
||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(Design.Spacing.large)
|
||||
}
|
||||
.background(AppSurface.primary)
|
||||
.navigationTitle(String(localized: "Choose Icon"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(String(localized: "Done")) {
|
||||
dismiss()
|
||||
}
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
RitualEditSheet(store: RitualStore.preview, ritual: nil)
|
||||
}
|
||||
@ -7,25 +7,28 @@ struct RitualFocusCardView: View {
|
||||
private let dayLabel: String
|
||||
private let completionSummary: String
|
||||
private let progress: Double
|
||||
private let iconName: String
|
||||
|
||||
init(
|
||||
title: String,
|
||||
theme: String,
|
||||
dayLabel: String,
|
||||
completionSummary: String,
|
||||
progress: Double
|
||||
progress: Double,
|
||||
iconName: String = "sparkles"
|
||||
) {
|
||||
self.title = title
|
||||
self.theme = theme
|
||||
self.dayLabel = dayLabel
|
||||
self.completionSummary = completionSummary
|
||||
self.progress = progress
|
||||
self.iconName = iconName
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
Image(systemName: "sparkles")
|
||||
Image(systemName: iconName)
|
||||
.font(.title2)
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
@ -17,13 +17,29 @@ struct TodayRitualSectionView: View {
|
||||
let completionSummary: String
|
||||
let progress: Double
|
||||
let habitRows: [HabitRowModel]
|
||||
var iconName: String = "sparkles"
|
||||
var timeOfDay: TimeOfDay = .anytime
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Bedrock.Design.Spacing.large) {
|
||||
SectionHeaderView(
|
||||
title: String(localized: "Focus ritual"),
|
||||
subtitle: String(localized: "Four-week arc in progress")
|
||||
)
|
||||
// Section header with time indicator
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: Bedrock.Design.Spacing.xSmall) {
|
||||
Text(focusTitle)
|
||||
.font(.headline)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Text(focusTheme)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Time of day indicator
|
||||
Image(systemName: timeOfDay.symbolName)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
.accessibilityLabel(timeOfDay.displayName)
|
||||
}
|
||||
|
||||
focusCard
|
||||
|
||||
@ -42,7 +58,8 @@ struct TodayRitualSectionView: View {
|
||||
theme: focusTheme,
|
||||
dayLabel: dayLabel,
|
||||
completionSummary: completionSummary,
|
||||
progress: progress
|
||||
progress: progress,
|
||||
iconName: iconName
|
||||
)
|
||||
.sherpaTag(RitualsOnboardingTag.focusRitual)
|
||||
}
|
||||
@ -80,6 +97,8 @@ struct TodayRitualSectionView: View {
|
||||
habitRows: [
|
||||
HabitRowModel(id: UUID(), title: "Hydrate", symbolName: "drop.fill", isCompleted: true, action: {}),
|
||||
HabitRowModel(id: UUID(), title: "Move", symbolName: "figure.walk", isCompleted: false, action: {})
|
||||
]
|
||||
],
|
||||
iconName: "sunrise.fill",
|
||||
timeOfDay: .morning
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,23 +3,32 @@ import Bedrock
|
||||
|
||||
struct TodayView: View {
|
||||
@Bindable var store: RitualStore
|
||||
|
||||
/// Rituals to show today: enabled, not archived, and appropriate for current time
|
||||
private var todayRituals: [Ritual] {
|
||||
store.ritualsForCurrentTime()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.large) {
|
||||
TodayHeaderView(dateText: store.todayDisplayString)
|
||||
|
||||
if let ritual = store.activeRitual {
|
||||
TodayRitualSectionView(
|
||||
focusTitle: ritual.title,
|
||||
focusTheme: ritual.theme,
|
||||
dayLabel: store.ritualDayLabel(for: ritual),
|
||||
completionSummary: store.completionSummary(for: ritual),
|
||||
progress: store.activeRitualProgress,
|
||||
habitRows: habitRows(for: ritual)
|
||||
)
|
||||
} else {
|
||||
if todayRituals.isEmpty {
|
||||
TodayEmptyStateView(store: store)
|
||||
} else {
|
||||
ForEach(todayRituals) { ritual in
|
||||
TodayRitualSectionView(
|
||||
focusTitle: ritual.title,
|
||||
focusTheme: ritual.theme,
|
||||
dayLabel: store.ritualDayLabel(for: ritual),
|
||||
completionSummary: store.completionSummary(for: ritual),
|
||||
progress: store.ritualProgress(for: ritual),
|
||||
habitRows: habitRows(for: ritual),
|
||||
iconName: ritual.iconName,
|
||||
timeOfDay: ritual.timeOfDay
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(Design.Spacing.large)
|
||||
|
||||
11
TODO.md
11
TODO.md
@ -41,7 +41,16 @@
|
||||
- [x] Filter by ritual using horizontal pill picker
|
||||
- [x] Tap day for detail sheet with habit list
|
||||
- [x] New History tab in tab bar
|
||||
- [ ] **Ritual management** – Create, edit, delete, and archive rituals.
|
||||
- [x] **Ritual management** – Create, edit, delete, and archive rituals. See plan: `.cursor/plans/ritual_management_system_1496c6a9.plan.md`
|
||||
- [x] Model enhancements (isEnabled, isArchived, timeOfDay, iconName, category)
|
||||
- [x] RitualStore CRUD methods (create, update, delete, enable, archive)
|
||||
- [x] Preset library with 13 categorized presets (Health, Productivity, Mindfulness, Self-Care)
|
||||
- [x] RitualsView toolbar menu (Create New, Browse Presets)
|
||||
- [x] RitualEditSheet for create/edit form with icon picker
|
||||
- [x] PresetLibrarySheet with category tabs and detail views
|
||||
- [x] RitualDetailView action menu (Edit, Enable/Disable, Archive, Delete)
|
||||
- [x] Destructive action confirmations with history warning
|
||||
- [x] Today view filtering by isEnabled, isArchived, and timeOfDay
|
||||
- [x] **Insights enhancements** – Weekly/monthly trends, streak data, charts. See plan: `.cursor/plans/insights_overhaul_50b59fa7.plan.md`
|
||||
- [x] Tappable insight cards with detail sheets
|
||||
- [x] Explanations for each metric
|
||||
|
||||
Loading…
Reference in New Issue
Block a user