Fix widget layout clipping and add comprehensive widget previews
- Fix LargeVotingView mood icons getting clipped at edges by using flexible HStack spacing with maxWidth: .infinity - Fix VotingView medium layout with smaller icons and even distribution - Add comprehensive #Preview macros for all widget states: - Vote widget: small/medium, voted/not voted, all mood states - Timeline widget: small/medium/large with various data states - Reduce icon sizes and padding to fit within widget bounds - Update accessibility labels and hints across views 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"sourceLanguage" : "en",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"strings" : {
|
||||||
"": {},
|
"" : {
|
||||||
|
|
||||||
|
},
|
||||||
" " : {
|
" " : {
|
||||||
"comment" : "A placeholder text used to create spacing between list items.",
|
"comment" : "A placeholder text used to create spacing between list items.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@@ -122,18 +124,18 @@
|
|||||||
"comment" : "The month and year displayed in the header of the calendar view. The first argument is the name of the month. The second argument is the last two digits of the year.",
|
"comment" : "The month and year displayed in the header of the calendar view. The first argument is the name of the month. The second argument is the last two digits of the year.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "new",
|
|
||||||
"value": "%1$@ '%2$@"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "%1$@ '%2$@"
|
"value" : "%1$@ '%2$@"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "%1$@ '%2$@"
|
||||||
|
}
|
||||||
|
},
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
@@ -170,18 +172,18 @@
|
|||||||
"comment" : "A month and year label displayed in a calendar view. The first argument is the name of the month. The second argument is the year.",
|
"comment" : "A month and year label displayed in a calendar view. The first argument is the name of the month. The second argument is the year.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "new",
|
|
||||||
"value": "%1$@ %2$@"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "%1$@ %2$@"
|
"value" : "%1$@ %2$@"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "%1$@ %2$@"
|
||||||
|
}
|
||||||
|
},
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
@@ -218,18 +220,18 @@
|
|||||||
"comment" : "A label that combines the mood description with the date it was recorded, separated by a space.",
|
"comment" : "A label that combines the mood description with the date it was recorded, separated by a space.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "new",
|
|
||||||
"value": "%1$@ on %2$@"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "%1$@ am %2$@"
|
"value" : "%1$@ am %2$@"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "%1$@ on %2$@"
|
||||||
|
}
|
||||||
|
},
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
@@ -266,18 +268,18 @@
|
|||||||
"comment" : "The current version of the app, displayed in a small, secondary font.",
|
"comment" : "The current version of the app, displayed in a small, secondary font.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "new",
|
|
||||||
"value": "%1$@ v %2$@ (Build %3$@)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "%1$@ v %2$@ (Build %3$@)"
|
"value" : "%1$@ v %2$@ (Build %3$@)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "%1$@ v %2$@ (Build %3$@)"
|
||||||
|
}
|
||||||
|
},
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
@@ -322,6 +324,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"%@, %@" : {
|
||||||
|
"comment" : "A button that, when tapped, selects or deselects a day option. The button's label is a combination of the title and subtitle.",
|
||||||
|
"isCommentAutoGenerated" : true,
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "%1$@, %2$@"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"%@, no mood logged" : {
|
||||||
|
"comment" : "A string that describes a day with no mood logged. The argument is the date string.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"%@: %@" : {
|
||||||
|
"comment" : "An element that represents a benefit of a service or product, with a title and a description.",
|
||||||
|
"isCommentAutoGenerated" : true,
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "%1$@: %2$@"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"%@%@" : {
|
"%@%@" : {
|
||||||
"comment" : "A text that displays the number of days remaining in the trial period, prefixed by \"Trial expires in \". The text is displayed in bold when the trial period is nearing its end.",
|
"comment" : "A text that displays the number of days remaining in the trial period, prefixed by \"Trial expires in \". The text is displayed in bold when the trial period is nearing its end.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -464,22 +494,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"%lld percent" : {
|
||||||
|
"comment" : "A value indicating the percentage of health data that has been successfully synced with Apple Health.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"%lld/%lld" : {
|
"%lld/%lld" : {
|
||||||
"comment" : "A text view showing the current number of characters in the note, followed by a slash and the maximum allowed number of characters.",
|
"comment" : "A text view showing the current number of characters in the note, followed by a slash and the maximum allowed number of characters.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "new",
|
|
||||||
"value": "%1$lld/%2$lld"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "%1$lld/%2$lld"
|
"value" : "%1$lld/%2$lld"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "%1$lld/%2$lld"
|
||||||
|
}
|
||||||
|
},
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
@@ -524,8 +558,12 @@
|
|||||||
"comment" : "A symbol that appears before a command in a terminal interface.",
|
"comment" : "A symbol that appears before a command in a terminal interface.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"12": {},
|
"12" : {
|
||||||
"17": {},
|
|
||||||
|
},
|
||||||
|
"17" : {
|
||||||
|
|
||||||
|
},
|
||||||
"20" : {
|
"20" : {
|
||||||
"comment" : "A placeholder text that appears in place of a number.",
|
"comment" : "A placeholder text that appears in place of a number.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@@ -1049,6 +1087,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Allow deleting mood entries by swiping" : {
|
||||||
|
"comment" : "A hint describing the functionality of the \"Allow deleting mood entries by swiping\" toggle.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Amazing! You have a %lld day streak. Keep it up!" : {
|
"Amazing! You have a %lld day streak. Keep it up!" : {
|
||||||
"comment" : "A response to a voice intent that confirms a user's current mood logging streak. The argument is the length of the streak.",
|
"comment" : "A response to a voice intent that confirms a user's current mood logging streak. The argument is the length of the streak.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -1091,6 +1133,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"App icon style %@" : {
|
||||||
|
"comment" : "A button that lets the user select an app icon style. The label shows the name of the style, without the \"AppIcon\" or \"Image\" prefix.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Apple Health" : {
|
"Apple Health" : {
|
||||||
"comment" : "The title of the toggle that enables or disables syncing mood data with Apple Health.",
|
"comment" : "The title of the toggle that enables or disables syncing mood data with Apple Health.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -1133,6 +1179,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Apple Health not available" : {
|
||||||
|
"comment" : "An accessibility label for the section of the settings view that indicates that Apple Health is not available on the user's device.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Are you sure you want to delete this mood entry? This cannot be undone." : {
|
"Are you sure you want to delete this mood entry? This cannot be undone." : {
|
||||||
"comment" : "An alert message displayed when the user attempts to delete a mood entry.",
|
"comment" : "An alert message displayed when the user attempts to delete a mood entry.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -3529,6 +3579,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Default app icon" : {
|
||||||
|
"comment" : "A description of the default app icon option in the icon picker.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"default_notif_body_today_four" : {
|
"default_notif_body_today_four" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -4424,6 +4478,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Double tap to select" : {
|
||||||
|
"comment" : "A hint that appears when tapping on an icon to select it.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Edit" : {
|
"Edit" : {
|
||||||
"comment" : "A button label that triggers the editing of a journal note.",
|
"comment" : "A button label that triggers the editing of a journal note.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -4844,6 +4902,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Export your mood data as CSV or PDF" : {
|
||||||
|
"comment" : "A hint that describes the functionality of the \"Export Data\" button in the Settings view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Exporting..." : {
|
"Exporting..." : {
|
||||||
"comment" : "A label indicating that a mood data export is in progress.",
|
"comment" : "A label indicating that a mood data export is in progress.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -6115,6 +6177,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Log this mood" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Log your mood daily to build a streak. Consistency helps you understand your patterns." : {
|
"Log your mood daily to build a streak. Consistency helps you understand your patterns." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -6576,6 +6641,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Mood logged: %@" : {
|
||||||
|
"comment" : "A label describing the logged mood. The argument is the logged mood.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Mood selection" : {
|
||||||
|
"comment" : "A heading for the mood selection section.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Mood Streak" : {
|
"Mood Streak" : {
|
||||||
"comment" : "Title of an app shortcut that shows the user their mood streak.",
|
"comment" : "Title of an app shortcut that shows the user their mood streak.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -8369,6 +8442,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Open app to subscribe" : {
|
||||||
|
"comment" : "A hint that appears when a user taps on a mood button in the voting view, explaining that they need to open the app to subscribe.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Open Feels" : {
|
"Open Feels" : {
|
||||||
"comment" : "Title of the app intent to open the Feels app.",
|
"comment" : "Title of the app intent to open the Feels app.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -8495,6 +8572,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Opens End User License Agreement in browser" : {
|
||||||
|
"comment" : "A button that opens the app's End User License Agreement in the user's web browser.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Opens Privacy Policy in browser" : {
|
||||||
|
"comment" : "A button that opens the app's privacy policy in a web browser.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Opens subscription options" : {
|
||||||
|
"comment" : "A hint for a button that opens a subscription store.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Opens time picker to change reminder time" : {
|
||||||
|
"comment" : "A hint that describes the action of tapping the \"Reminder Time\" button in the Settings view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Or use your device passcode" : {
|
"Or use your device passcode" : {
|
||||||
"comment" : "A hint displayed below the \"Unlock with biometric\" button, encouraging users to use their device passcode if they don't have a biometric authentication method set up.",
|
"comment" : "A hint displayed below the \"Unlock with biometric\" button, encouraging users to use their device passcode if they don't have a biometric authentication method set up.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -8871,6 +8964,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Premium feature, subscription required" : {
|
||||||
|
"comment" : "A description of a premium feature that requires a subscription.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Privacy Lock" : {
|
"Privacy Lock" : {
|
||||||
"comment" : "A title for a toggle that controls whether or not biometric authentication is enabled.",
|
"comment" : "A title for a toggle that controls whether or not biometric authentication is enabled.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -9823,6 +9920,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Reminder time" : {
|
||||||
|
"comment" : "An accessibility label for the time picker in the onboarding flow.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Reminder Time" : {
|
"Reminder Time" : {
|
||||||
"comment" : "A label displayed above the reminder time in the settings view.",
|
"comment" : "A label displayed above the reminder time in the settings view.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -9949,6 +10050,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Require biometric authentication to open app" : {
|
||||||
|
"comment" : "A hint that describes the purpose of the Privacy Lock toggle.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Reset luanch date to current date" : {
|
"Reset luanch date to current date" : {
|
||||||
"comment" : "A button label that resets the app's launch date to the current date.",
|
"comment" : "A button label that resets the app's launch date to the current date.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -10676,6 +10781,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Select this mood" : {
|
||||||
|
"comment" : "A hint that appears when a user taps on a mood button.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Select when you want to be reminded" : {
|
||||||
|
"comment" : "A hint that appears when a user taps on the time picker in the \"Reminder time\" section of the OnboardingTime view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Set Trial Start Date" : {
|
"Set Trial Start Date" : {
|
||||||
"comment" : "The title of a screen that lets a user set the start date of a free trial.",
|
"comment" : "The title of a screen that lets a user set the start date of a free trial.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -11450,6 +11563,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Skip subscription and complete setup" : {
|
||||||
|
"comment" : "A button label that says \"Skip subscription and complete setup\". It's used in the \"OnboardingSubscription\" view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Streak: %lld days" : {
|
"Streak: %lld days" : {
|
||||||
"comment" : "A label in the expanded view that describes the current streak of days the user has logged in.",
|
"comment" : "A label in the expanded view that describes the current streak of days the user has logged in.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -11811,6 +11928,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Swipe right to continue" : {
|
||||||
|
"comment" : "A hint that appears when a user swipes to the next onboarding step.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Swipe to get started" : {
|
"Swipe to get started" : {
|
||||||
"comment" : "A hint displayed below the feature rows, instructing users to swipe to continue.",
|
"comment" : "A hint displayed below the feature rows, instructing users to swipe to continue.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -11853,6 +11974,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Swipe to the next onboarding step" : {
|
||||||
|
"comment" : "An accessibility hint that describes the action to take to progress to the next onboarding step.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Switch between Day, Month, and Year views to see your mood patterns over time." : {
|
"Switch between Day, Month, and Year views to see your mood patterns over time." : {
|
||||||
"comment" : "A tip that instructs the user to switch between different time views to view their mood history.",
|
"comment" : "A tip that instructs the user to switch between different time views to view their mood history.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -11895,6 +12020,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Sync mood data with Apple Health" : {
|
||||||
|
"comment" : "A hint that appears when the user taps the toggle to sync mood data with Apple Health.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Sync with Apple Health" : {
|
"Sync with Apple Health" : {
|
||||||
"comment" : "A tip to sync their data with Apple Health.",
|
"comment" : "A tip to sync their data with Apple Health.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -11937,6 +12066,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Syncing health data" : {
|
||||||
|
"comment" : "A label indicating that health data is being synced.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Take Photo" : {
|
"Take Photo" : {
|
||||||
"comment" : "A button that takes a photo using the device's camera.",
|
"comment" : "A button that takes a photo using the device's camera.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -12147,6 +12280,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Tap to log mood for this day" : {
|
||||||
|
"comment" : "A hint that appears when a user taps on a day with no mood logged, instructing them to log a mood.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Tap to log your mood" : {
|
"Tap to log your mood" : {
|
||||||
"comment" : "A description of an action a user can take to log their mood.",
|
"comment" : "A description of an action a user can take to log their mood.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -12189,6 +12326,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Tap to open app and subscribe" : {
|
||||||
|
"comment" : "A hint that describes the action to subscribe to the widget.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Tap to record your mood for this day" : {
|
"Tap to record your mood for this day" : {
|
||||||
"comment" : "A description of what a user can do to add a new entry to their mood journal.",
|
"comment" : "A description of what a user can do to add a new entry to their mood journal.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -12273,6 +12414,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Tap to view or edit" : {
|
||||||
|
"comment" : "A hint that appears when a user taps on an entry to view or edit it.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Test builds only" : {
|
"Test builds only" : {
|
||||||
"comment" : "A section header that indicates that the settings view contains only test data.",
|
"comment" : "A section header that indicates that the settings view contains only test data.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -13151,6 +13296,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"View the app introduction again" : {
|
||||||
|
"comment" : "A button that allows a user to view the app's introductory screen again.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"View Your History" : {
|
"View Your History" : {
|
||||||
"comment" : "A tip title for viewing and managing one's mood history.",
|
"comment" : "A tip title for viewing and managing one's mood history.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|||||||
@@ -227,31 +227,31 @@ struct VotingView: View {
|
|||||||
|
|
||||||
// MARK: - Medium Widget: Single row
|
// MARK: - Medium Widget: Single row
|
||||||
private var mediumLayout: some View {
|
private var mediumLayout: some View {
|
||||||
VStack {
|
VStack(spacing: 12) {
|
||||||
Text(hasSubscription ? promptText : "Subscribe to track your mood")
|
Text(hasSubscription ? promptText : "Subscribe to track your mood")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.minimumScaleFactor(0.8)
|
.minimumScaleFactor(0.8)
|
||||||
.padding(.bottom, 20)
|
|
||||||
|
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 0) {
|
||||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
||||||
moodButton(for: mood, size: 44)
|
moodButtonMedium(for: mood)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func moodButton(for mood: Mood, size: CGFloat) -> some View {
|
private func moodButton(for mood: Mood, size: CGFloat) -> some View {
|
||||||
// Ensure minimum 44x44 touch target for accessibility
|
// Used for small widget
|
||||||
let touchSize = max(size, 44)
|
let touchSize = max(size, 44)
|
||||||
|
|
||||||
if hasSubscription {
|
if hasSubscription {
|
||||||
// Active subscription: vote normally
|
|
||||||
Button(intent: VoteMoodIntent(mood: mood)) {
|
Button(intent: VoteMoodIntent(mood: mood)) {
|
||||||
moodIcon(for: mood, size: size)
|
moodIcon(for: mood, size: size)
|
||||||
.frame(minWidth: touchSize, minHeight: touchSize)
|
.frame(minWidth: touchSize, minHeight: touchSize)
|
||||||
@@ -260,7 +260,6 @@ struct VotingView: View {
|
|||||||
.accessibilityLabel(mood.strValue)
|
.accessibilityLabel(mood.strValue)
|
||||||
.accessibilityHint(String(localized: "Log this mood"))
|
.accessibilityHint(String(localized: "Log this mood"))
|
||||||
} else {
|
} else {
|
||||||
// Trial expired: open app to subscribe
|
|
||||||
Link(destination: URL(string: "feels://subscribe")!) {
|
Link(destination: URL(string: "feels://subscribe")!) {
|
||||||
moodIcon(for: mood, size: size)
|
moodIcon(for: mood, size: size)
|
||||||
.frame(minWidth: touchSize, minHeight: touchSize)
|
.frame(minWidth: touchSize, minHeight: touchSize)
|
||||||
@@ -270,6 +269,39 @@ struct VotingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func moodButtonMedium(for mood: Mood) -> some View {
|
||||||
|
// Medium widget uses smaller icons with labels, flexible width
|
||||||
|
let content = VStack(spacing: 4) {
|
||||||
|
moodImages.icon(forMood: mood)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.foregroundColor(moodTint.color(forMood: mood))
|
||||||
|
|
||||||
|
Text(mood.widgetDisplayName)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(moodTint.color(forMood: mood))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasSubscription {
|
||||||
|
Button(intent: VoteMoodIntent(mood: mood)) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
|
.accessibilityHint(String(localized: "Log this mood"))
|
||||||
|
} else {
|
||||||
|
Link(destination: URL(string: "feels://subscribe")!) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
|
.accessibilityHint(String(localized: "Open app to subscribe"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func moodIcon(for mood: Mood, size: CGFloat) -> some View {
|
private func moodIcon(for mood: Mood, size: CGFloat) -> some View {
|
||||||
moodImages.icon(forMood: mood)
|
moodImages.icon(forMood: mood)
|
||||||
.resizable()
|
.resizable()
|
||||||
@@ -315,11 +347,13 @@ struct VotedStatsView: View {
|
|||||||
|
|
||||||
// Checkmark badge
|
// Checkmark badge
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.font(.system(size: 18))
|
.font(.headline)
|
||||||
.foregroundColor(.green)
|
.foregroundColor(.green)
|
||||||
.background(Circle().fill(.white).frame(width: 14, height: 14))
|
.background(Circle().fill(.white).frame(width: 14, height: 14))
|
||||||
.offset(x: 4, y: 4)
|
.offset(x: 4, y: 4)
|
||||||
}
|
}
|
||||||
|
.accessibilityElement(children: .combine)
|
||||||
|
.accessibilityLabel(String(localized: "Mood logged: \(mood.strValue)"))
|
||||||
|
|
||||||
Text("Logged!")
|
Text("Logged!")
|
||||||
.font(.caption.weight(.semibold))
|
.font(.caption.weight(.semibold))
|
||||||
@@ -331,8 +365,6 @@ struct VotedStatsView: View {
|
|||||||
.foregroundStyle(.tertiary)
|
.foregroundStyle(.tertiary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.accessibilityElement(children: .combine)
|
|
||||||
.accessibilityLabel(String(localized: "Mood logged: \(entry.todaysMood?.strValue ?? "")"))
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.padding(12)
|
.padding(12)
|
||||||
@@ -340,7 +372,7 @@ struct VotedStatsView: View {
|
|||||||
|
|
||||||
// MARK: - Medium: Mood + stats bar
|
// MARK: - Medium: Mood + stats bar
|
||||||
private var mediumLayout: some View {
|
private var mediumLayout: some View {
|
||||||
HStack(spacing: 20) {
|
HStack(alignment: .top, spacing: 20) {
|
||||||
if let mood = entry.todaysMood {
|
if let mood = entry.todaysMood {
|
||||||
// Left: Mood display
|
// Left: Mood display
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
@@ -349,6 +381,7 @@ struct VotedStatsView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 48, height: 48)
|
.frame(width: 48, height: 48)
|
||||||
.foregroundColor(moodTint.color(forMood: mood))
|
.foregroundColor(moodTint.color(forMood: mood))
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
|
|
||||||
Text(mood.widgetDisplayName)
|
Text(mood.widgetDisplayName)
|
||||||
.font(.subheadline.weight(.semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
@@ -359,11 +392,11 @@ struct VotedStatsView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right: Stats
|
// Right: Stats with progress bar aligned under title
|
||||||
if let stats = entry.stats {
|
if let stats = entry.stats {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text("\(stats.totalEntries) entries")
|
Text("\(stats.totalEntries) entries")
|
||||||
.font(.caption.weight(.medium))
|
.font(.headline.weight(.semibold))
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
|
|
||||||
// Mini mood breakdown
|
// Mini mood breakdown
|
||||||
@@ -383,21 +416,21 @@ struct VotedStatsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Progress bar
|
// Progress bar - aligned with title
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
HStack(spacing: 1) {
|
HStack(spacing: 1) {
|
||||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { m in
|
||||||
let percentage = stats.percentage(for: mood)
|
let percentage = stats.percentage(for: m)
|
||||||
if percentage > 0 {
|
if percentage > 0 {
|
||||||
RoundedRectangle(cornerRadius: 2)
|
RoundedRectangle(cornerRadius: 2)
|
||||||
.fill(moodTint.color(forMood: mood))
|
.fill(moodTint.color(forMood: m))
|
||||||
.frame(width: max(4, geo.size.width * CGFloat(percentage) / 100))
|
.frame(width: max(4, geo.size.width * CGFloat(percentage) / 100))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: 8)
|
.frame(height: 10)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
@@ -449,12 +482,202 @@ struct FeelsVoteWidget: Widget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Preview
|
// MARK: - Preview Helpers
|
||||||
|
|
||||||
#Preview(as: .systemSmall) {
|
private enum VoteWidgetPreviewHelpers {
|
||||||
|
static let sampleStats = MoodStats(
|
||||||
|
totalEntries: 30,
|
||||||
|
moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]
|
||||||
|
)
|
||||||
|
|
||||||
|
static let largeStats = MoodStats(
|
||||||
|
totalEntries: 100,
|
||||||
|
moodCounts: [.great: 35, .good: 40, .average: 15, .bad: 7, .horrible: 3]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Small Widget Previews
|
||||||
|
|
||||||
|
#Preview("Vote Small - Not Voted", as: .systemSmall) {
|
||||||
FeelsVoteWidget()
|
FeelsVoteWidget()
|
||||||
} timeline: {
|
} timeline: {
|
||||||
VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: false, todaysMood: nil, stats: nil, promptText: "How are you feeling today?")
|
VoteWidgetEntry(
|
||||||
VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .great, stats: MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]), promptText: "")
|
date: Date(),
|
||||||
VoteWidgetEntry(date: Date(), hasSubscription: false, hasVotedToday: false, todaysMood: nil, stats: nil, promptText: "")
|
hasSubscription: true,
|
||||||
|
hasVotedToday: false,
|
||||||
|
todaysMood: nil,
|
||||||
|
stats: nil,
|
||||||
|
promptText: "How are you feeling today?"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Vote Small - Voted Great", as: .systemSmall) {
|
||||||
|
FeelsVoteWidget()
|
||||||
|
} timeline: {
|
||||||
|
VoteWidgetEntry(
|
||||||
|
date: Date(),
|
||||||
|
hasSubscription: true,
|
||||||
|
hasVotedToday: true,
|
||||||
|
todaysMood: .great,
|
||||||
|
stats: VoteWidgetPreviewHelpers.sampleStats,
|
||||||
|
promptText: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Vote Small - Voted Good", as: .systemSmall) {
|
||||||
|
FeelsVoteWidget()
|
||||||
|
} timeline: {
|
||||||
|
VoteWidgetEntry(
|
||||||
|
date: Date(),
|
||||||
|
hasSubscription: true,
|
||||||
|
hasVotedToday: true,
|
||||||
|
todaysMood: .good,
|
||||||
|
stats: VoteWidgetPreviewHelpers.sampleStats,
|
||||||
|
promptText: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Vote Small - Voted Average", as: .systemSmall) {
|
||||||
|
FeelsVoteWidget()
|
||||||
|
} timeline: {
|
||||||
|
VoteWidgetEntry(
|
||||||
|
date: Date(),
|
||||||
|
hasSubscription: true,
|
||||||
|
hasVotedToday: true,
|
||||||
|
todaysMood: .average,
|
||||||
|
stats: VoteWidgetPreviewHelpers.sampleStats,
|
||||||
|
promptText: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Vote Small - Voted Bad", as: .systemSmall) {
|
||||||
|
FeelsVoteWidget()
|
||||||
|
} timeline: {
|
||||||
|
VoteWidgetEntry(
|
||||||
|
date: Date(),
|
||||||
|
hasSubscription: true,
|
||||||
|
hasVotedToday: true,
|
||||||
|
todaysMood: .bad,
|
||||||
|
stats: VoteWidgetPreviewHelpers.sampleStats,
|
||||||
|
promptText: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Vote Small - Voted Horrible", as: .systemSmall) {
|
||||||
|
FeelsVoteWidget()
|
||||||
|
} timeline: {
|
||||||
|
VoteWidgetEntry(
|
||||||
|
date: Date(),
|
||||||
|
hasSubscription: true,
|
||||||
|
hasVotedToday: true,
|
||||||
|
todaysMood: .horrible,
|
||||||
|
stats: VoteWidgetPreviewHelpers.sampleStats,
|
||||||
|
promptText: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Vote Small - Non-Subscriber", as: .systemSmall) {
|
||||||
|
FeelsVoteWidget()
|
||||||
|
} timeline: {
|
||||||
|
VoteWidgetEntry(
|
||||||
|
date: Date(),
|
||||||
|
hasSubscription: false,
|
||||||
|
hasVotedToday: false,
|
||||||
|
todaysMood: nil,
|
||||||
|
stats: nil,
|
||||||
|
promptText: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Medium Widget Previews
|
||||||
|
|
||||||
|
#Preview("Vote Medium - Not Voted", as: .systemMedium) {
|
||||||
|
FeelsVoteWidget()
|
||||||
|
} timeline: {
|
||||||
|
VoteWidgetEntry(
|
||||||
|
date: Date(),
|
||||||
|
hasSubscription: true,
|
||||||
|
hasVotedToday: false,
|
||||||
|
todaysMood: nil,
|
||||||
|
stats: nil,
|
||||||
|
promptText: "How are you feeling today?"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Vote Medium - Voted Great", as: .systemMedium) {
|
||||||
|
FeelsVoteWidget()
|
||||||
|
} timeline: {
|
||||||
|
VoteWidgetEntry(
|
||||||
|
date: Date(),
|
||||||
|
hasSubscription: true,
|
||||||
|
hasVotedToday: true,
|
||||||
|
todaysMood: .great,
|
||||||
|
stats: VoteWidgetPreviewHelpers.largeStats,
|
||||||
|
promptText: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Vote Medium - Voted Good", as: .systemMedium) {
|
||||||
|
FeelsVoteWidget()
|
||||||
|
} timeline: {
|
||||||
|
VoteWidgetEntry(
|
||||||
|
date: Date(),
|
||||||
|
hasSubscription: true,
|
||||||
|
hasVotedToday: true,
|
||||||
|
todaysMood: .good,
|
||||||
|
stats: VoteWidgetPreviewHelpers.largeStats,
|
||||||
|
promptText: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Vote Medium - Voted Average", as: .systemMedium) {
|
||||||
|
FeelsVoteWidget()
|
||||||
|
} timeline: {
|
||||||
|
VoteWidgetEntry(
|
||||||
|
date: Date(),
|
||||||
|
hasSubscription: true,
|
||||||
|
hasVotedToday: true,
|
||||||
|
todaysMood: .average,
|
||||||
|
stats: VoteWidgetPreviewHelpers.sampleStats,
|
||||||
|
promptText: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Vote Medium - Voted Bad", as: .systemMedium) {
|
||||||
|
FeelsVoteWidget()
|
||||||
|
} timeline: {
|
||||||
|
VoteWidgetEntry(
|
||||||
|
date: Date(),
|
||||||
|
hasSubscription: true,
|
||||||
|
hasVotedToday: true,
|
||||||
|
todaysMood: .bad,
|
||||||
|
stats: VoteWidgetPreviewHelpers.sampleStats,
|
||||||
|
promptText: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Vote Medium - Voted Horrible", as: .systemMedium) {
|
||||||
|
FeelsVoteWidget()
|
||||||
|
} timeline: {
|
||||||
|
VoteWidgetEntry(
|
||||||
|
date: Date(),
|
||||||
|
hasSubscription: true,
|
||||||
|
hasVotedToday: true,
|
||||||
|
todaysMood: .horrible,
|
||||||
|
stats: VoteWidgetPreviewHelpers.sampleStats,
|
||||||
|
promptText: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Vote Medium - Non-Subscriber", as: .systemMedium) {
|
||||||
|
FeelsVoteWidget()
|
||||||
|
} timeline: {
|
||||||
|
VoteWidgetEntry(
|
||||||
|
date: Date(),
|
||||||
|
hasSubscription: false,
|
||||||
|
hasVotedToday: false,
|
||||||
|
todaysMood: nil,
|
||||||
|
stats: nil,
|
||||||
|
promptText: ""
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,13 +138,15 @@ class WatchTimelineView: Identifiable {
|
|||||||
let date: Date
|
let date: Date
|
||||||
let color: Color
|
let color: Color
|
||||||
let secondaryColor: Color
|
let secondaryColor: Color
|
||||||
|
let mood: Mood
|
||||||
|
|
||||||
init(image: Image, graphic: Image, date: Date, color: Color, secondaryColor: Color) {
|
init(image: Image, graphic: Image, date: Date, color: Color, secondaryColor: Color, mood: Mood) {
|
||||||
self.image = image
|
self.image = image
|
||||||
self.date = date
|
self.date = date
|
||||||
self.color = color
|
self.color = color
|
||||||
self.graphic = graphic
|
self.graphic = graphic
|
||||||
self.secondaryColor = secondaryColor
|
self.secondaryColor = secondaryColor
|
||||||
|
self.mood = mood
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,13 +173,15 @@ struct TimeLineCreator {
|
|||||||
graphic: moodImages.icon(forMood: todayEntry.mood),
|
graphic: moodImages.icon(forMood: todayEntry.mood),
|
||||||
date: dayStart,
|
date: dayStart,
|
||||||
color: moodTint.color(forMood: todayEntry.mood),
|
color: moodTint.color(forMood: todayEntry.mood),
|
||||||
secondaryColor: moodTint.secondary(forMood: todayEntry.mood)))
|
secondaryColor: moodTint.secondary(forMood: todayEntry.mood),
|
||||||
|
mood: todayEntry.mood))
|
||||||
} else {
|
} else {
|
||||||
timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: .missing),
|
timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: .missing),
|
||||||
graphic: moodImages.icon(forMood: .missing),
|
graphic: moodImages.icon(forMood: .missing),
|
||||||
date: dayStart,
|
date: dayStart,
|
||||||
color: moodTint.color(forMood: .missing),
|
color: moodTint.color(forMood: .missing),
|
||||||
secondaryColor: moodTint.secondary(forMood: .missing)))
|
secondaryColor: moodTint.secondary(forMood: .missing),
|
||||||
|
mood: .missing))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +206,8 @@ struct TimeLineCreator {
|
|||||||
graphic: moodImages.icon(forMood: mood),
|
graphic: moodImages.icon(forMood: mood),
|
||||||
date: dayStart,
|
date: dayStart,
|
||||||
color: moodTint.color(forMood: mood),
|
color: moodTint.color(forMood: mood),
|
||||||
secondaryColor: moodTint.secondary(forMood: mood)
|
secondaryColor: moodTint.secondary(forMood: mood),
|
||||||
|
mood: mood
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,6 +382,7 @@ struct SmallWidgetView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 70, height: 70)
|
.frame(width: 70, height: 70)
|
||||||
.foregroundColor(today.color)
|
.foregroundColor(today.color)
|
||||||
|
.accessibilityLabel(today.mood.strValue)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 12)
|
.frame(height: 12)
|
||||||
@@ -470,7 +476,8 @@ struct MediumWidgetView: View {
|
|||||||
image: item.image,
|
image: item.image,
|
||||||
color: item.color,
|
color: item.color,
|
||||||
isToday: index == 0,
|
isToday: index == 0,
|
||||||
height: cellHeight
|
height: cellHeight,
|
||||||
|
mood: item.mood
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -491,6 +498,7 @@ struct MediumDayCell: View {
|
|||||||
let color: Color
|
let color: Color
|
||||||
let isToday: Bool
|
let isToday: Bool
|
||||||
let height: CGFloat
|
let height: CGFloat
|
||||||
|
let mood: Mood
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -500,7 +508,7 @@ struct MediumDayCell: View {
|
|||||||
|
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
Text(dayLabel)
|
Text(dayLabel)
|
||||||
.font(.system(size: 10, weight: isToday ? .bold : .medium))
|
.font(.caption2.weight(isToday ? .bold : .medium))
|
||||||
.foregroundStyle(isToday ? .primary : .secondary)
|
.foregroundStyle(isToday ? .primary : .secondary)
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
|
|
||||||
@@ -509,9 +517,10 @@ struct MediumDayCell: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 36, height: 36)
|
.frame(width: 36, height: 36)
|
||||||
.foregroundColor(color)
|
.foregroundColor(color)
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
|
|
||||||
Text(dateLabel)
|
Text(dateLabel)
|
||||||
.font(.system(size: 13, weight: isToday ? .bold : .semibold))
|
.font(.caption.weight(isToday ? .bold : .semibold))
|
||||||
.foregroundStyle(isToday ? color : .secondary)
|
.foregroundStyle(isToday ? color : .secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -584,7 +593,8 @@ struct LargeWidgetView: View {
|
|||||||
image: item.image,
|
image: item.image,
|
||||||
color: item.color,
|
color: item.color,
|
||||||
isToday: index == 0,
|
isToday: index == 0,
|
||||||
height: cellHeight
|
height: cellHeight,
|
||||||
|
mood: item.mood
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -598,7 +608,8 @@ struct LargeWidgetView: View {
|
|||||||
image: item.image,
|
image: item.image,
|
||||||
color: item.color,
|
color: item.color,
|
||||||
isToday: false,
|
isToday: false,
|
||||||
height: cellHeight
|
height: cellHeight,
|
||||||
|
mood: item.mood
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,11 +645,12 @@ struct DayCell: View {
|
|||||||
let color: Color
|
let color: Color
|
||||||
let isToday: Bool
|
let isToday: Bool
|
||||||
let height: CGFloat
|
let height: CGFloat
|
||||||
|
let mood: Mood
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 2) {
|
VStack(spacing: 2) {
|
||||||
Text(dayLabel)
|
Text(dayLabel)
|
||||||
.font(.system(size: 10, weight: isToday ? .bold : .medium))
|
.font(.caption2.weight(isToday ? .bold : .medium))
|
||||||
.foregroundStyle(isToday ? .primary : .secondary)
|
.foregroundStyle(isToday ? .primary : .secondary)
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
|
|
||||||
@@ -653,9 +665,10 @@ struct DayCell: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 38, height: 38)
|
.frame(width: 38, height: 38)
|
||||||
.foregroundColor(color)
|
.foregroundColor(color)
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
|
|
||||||
Text(dateLabel)
|
Text(dateLabel)
|
||||||
.font(.system(size: 13, weight: isToday ? .bold : .semibold))
|
.font(.caption.weight(isToday ? .bold : .semibold))
|
||||||
.foregroundStyle(isToday ? color : .secondary)
|
.foregroundStyle(isToday ? color : .secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -679,26 +692,29 @@ struct LargeVotingView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 24) {
|
VStack(spacing: 16) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text(hasSubscription ? promptText : "Subscribe to track your mood")
|
Text(hasSubscription ? promptText : "Subscribe to track your mood")
|
||||||
.font(.title2.weight(.semibold))
|
.font(.title3.weight(.semibold))
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.minimumScaleFactor(0.8)
|
.minimumScaleFactor(0.8)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
|
||||||
// Large mood buttons in a row
|
// Large mood buttons in a row - flexible spacing
|
||||||
HStack(spacing: 20) {
|
HStack(spacing: 0) {
|
||||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
||||||
moodButton(for: mood)
|
moodButton(for: mood)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -708,29 +724,35 @@ struct LargeVotingView: View {
|
|||||||
moodButtonContent(for: mood)
|
moodButtonContent(for: mood)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
|
.accessibilityHint(String(localized: "Log this mood"))
|
||||||
} else {
|
} else {
|
||||||
Link(destination: URL(string: "feels://subscribe")!) {
|
Link(destination: URL(string: "feels://subscribe")!) {
|
||||||
moodButtonContent(for: mood)
|
moodButtonContent(for: mood)
|
||||||
}
|
}
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
|
.accessibilityHint(String(localized: "Open app to subscribe"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func moodButtonContent(for mood: Mood) -> some View {
|
private func moodButtonContent(for mood: Mood) -> some View {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 4) {
|
||||||
moodImages.icon(forMood: mood)
|
moodImages.icon(forMood: mood)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 56, height: 56)
|
.frame(width: 40, height: 40)
|
||||||
.foregroundColor(moodTint.color(forMood: mood))
|
.foregroundColor(moodTint.color(forMood: mood))
|
||||||
|
|
||||||
Text(mood.strValue)
|
Text(mood.widgetDisplayName)
|
||||||
.font(.caption.weight(.medium))
|
.font(.caption2.weight(.medium))
|
||||||
.foregroundColor(moodTint.color(forMood: mood))
|
.foregroundColor(moodTint.color(forMood: mood))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.8)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 8)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 4)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 16)
|
RoundedRectangle(cornerRadius: 12)
|
||||||
.fill(moodTint.color(forMood: mood).opacity(0.15))
|
.fill(moodTint.color(forMood: mood).opacity(0.15))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -899,10 +921,14 @@ struct InlineVotingView: View {
|
|||||||
moodIcon(for: mood)
|
moodIcon(for: mood)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
|
.accessibilityHint(String(localized: "Log this mood"))
|
||||||
} else {
|
} else {
|
||||||
Link(destination: URL(string: "feels://subscribe")!) {
|
Link(destination: URL(string: "feels://subscribe")!) {
|
||||||
moodIcon(for: mood)
|
moodIcon(for: mood)
|
||||||
}
|
}
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
|
.accessibilityHint(String(localized: "Open app to subscribe"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -924,6 +950,7 @@ struct EntryCard: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 50, height: 50, alignment: .center)
|
.frame(width: 50, height: 50, alignment: .center)
|
||||||
.foregroundColor(timeLineView.color)
|
.foregroundColor(timeLineView.color)
|
||||||
|
.accessibilityLabel(timeLineView.mood.strValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1011,115 +1038,364 @@ struct FeelsGraphicWidget: Widget {
|
|||||||
|
|
||||||
// MARK: - Preview Helpers
|
// MARK: - Preview Helpers
|
||||||
|
|
||||||
private extension FeelsWidget_Previews {
|
private enum WidgetPreviewHelpers {
|
||||||
static func sampleTimelineViews(count: Int) -> [WatchTimelineView] {
|
static func sampleTimelineViews(count: Int, startMood: Mood = .great) -> [WatchTimelineView] {
|
||||||
let moods: [Mood] = [.great, .good, .average, .bad, .horrible]
|
let moods: [Mood] = [.great, .good, .average, .bad, .horrible]
|
||||||
|
let startIndex = moods.firstIndex(of: startMood) ?? 0
|
||||||
return (0..<count).map { index in
|
return (0..<count).map { index in
|
||||||
let mood = moods[index % moods.count]
|
let mood = moods[(startIndex + index) % moods.count]
|
||||||
return WatchTimelineView(
|
return WatchTimelineView(
|
||||||
image: EmojiMoodImages.icon(forMood: mood),
|
image: EmojiMoodImages.icon(forMood: mood),
|
||||||
graphic: EmojiMoodImages.icon(forMood: mood),
|
graphic: EmojiMoodImages.icon(forMood: mood),
|
||||||
date: Calendar.current.date(byAdding: .day, value: -index, to: Date())!,
|
date: Calendar.current.date(byAdding: .day, value: -index, to: Date())!,
|
||||||
color: MoodTints.Default.color(forMood: mood),
|
color: MoodTints.Default.color(forMood: mood),
|
||||||
secondaryColor: MoodTints.Default.secondary(forMood: mood)
|
secondaryColor: MoodTints.Default.secondary(forMood: mood),
|
||||||
|
mood: mood
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func sampleEntry(timelineCount: Int = 5) -> SimpleEntry {
|
static func sampleEntry(timelineCount: Int = 5, hasVotedToday: Bool = true, hasSubscription: Bool = true, startMood: Mood = .great) -> SimpleEntry {
|
||||||
SimpleEntry(
|
SimpleEntry(
|
||||||
date: Date(),
|
date: Date(),
|
||||||
configuration: ConfigurationIntent(),
|
configuration: ConfigurationIntent(),
|
||||||
timeLineViews: sampleTimelineViews(count: timelineCount),
|
timeLineViews: sampleTimelineViews(count: timelineCount, startMood: startMood),
|
||||||
hasSubscription: true,
|
hasSubscription: hasSubscription,
|
||||||
hasVotedToday: true
|
hasVotedToday: hasVotedToday,
|
||||||
|
promptText: "How are you feeling today?"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FeelsWidget_Previews: PreviewProvider {
|
// MARK: - FeelsWidget Previews (Timeline Widget)
|
||||||
static var previews: some View {
|
|
||||||
Group {
|
|
||||||
// MARK: - FeelsWidget (Timeline)
|
|
||||||
FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 1))
|
|
||||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
|
||||||
.previewDisplayName("Timeline - Small")
|
|
||||||
|
|
||||||
FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 5))
|
// Small - Logged States
|
||||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
#Preview("Timeline Small - Great", as: .systemSmall) {
|
||||||
.previewDisplayName("Timeline - Medium")
|
FeelsWidget()
|
||||||
|
} timeline: {
|
||||||
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .great)
|
||||||
|
}
|
||||||
|
|
||||||
FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 10))
|
#Preview("Timeline Small - Good", as: .systemSmall) {
|
||||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
FeelsWidget()
|
||||||
.previewDisplayName("Timeline - Large")
|
} timeline: {
|
||||||
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .good)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - FeelsGraphicWidget (Mood Graphic)
|
#Preview("Timeline Small - Average", as: .systemSmall) {
|
||||||
FeelsGraphicWidgetEntryView(entry: sampleEntry(timelineCount: 2))
|
FeelsWidget()
|
||||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
} timeline: {
|
||||||
.previewDisplayName("Mood Graphic - Small")
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .average)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - FeelsIconWidget (Custom Icon)
|
#Preview("Timeline Small - Bad", as: .systemSmall) {
|
||||||
FeelsIconWidgetEntryView(entry: sampleEntry())
|
FeelsWidget()
|
||||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
} timeline: {
|
||||||
.previewDisplayName("Custom Icon - Small")
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .bad)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - FeelsVoteWidget (Vote - Not Voted)
|
#Preview("Timeline Small - Horrible", as: .systemSmall) {
|
||||||
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry(
|
FeelsWidget()
|
||||||
date: Date(),
|
} timeline: {
|
||||||
hasSubscription: true,
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .horrible)
|
||||||
hasVotedToday: false,
|
}
|
||||||
todaysMood: nil,
|
|
||||||
stats: nil,
|
|
||||||
promptText: "How are you feeling?"
|
|
||||||
))
|
|
||||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
|
||||||
.previewDisplayName("Vote - Small (Not Voted)")
|
|
||||||
|
|
||||||
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry(
|
// Small - Voting States
|
||||||
date: Date(),
|
#Preview("Timeline Small - Voting", as: .systemSmall) {
|
||||||
hasSubscription: true,
|
FeelsWidget()
|
||||||
hasVotedToday: false,
|
} timeline: {
|
||||||
todaysMood: nil,
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, hasVotedToday: false)
|
||||||
stats: nil,
|
}
|
||||||
promptText: "How are you feeling?"
|
|
||||||
))
|
|
||||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
|
||||||
.previewDisplayName("Vote - Medium (Not Voted)")
|
|
||||||
|
|
||||||
// MARK: - FeelsVoteWidget (Vote - Already Voted)
|
#Preview("Timeline Small - Non-Subscriber", as: .systemSmall) {
|
||||||
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry(
|
FeelsWidget()
|
||||||
date: Date(),
|
} timeline: {
|
||||||
hasSubscription: true,
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, hasVotedToday: false, hasSubscription: false)
|
||||||
hasVotedToday: true,
|
}
|
||||||
todaysMood: .great,
|
|
||||||
stats: MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]),
|
|
||||||
promptText: ""
|
|
||||||
))
|
|
||||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
|
||||||
.previewDisplayName("Vote - Small (Voted)")
|
|
||||||
|
|
||||||
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry(
|
// Medium - Logged States
|
||||||
date: Date(),
|
#Preview("Timeline Medium - Logged", as: .systemMedium) {
|
||||||
hasSubscription: true,
|
FeelsWidget()
|
||||||
hasVotedToday: true,
|
} timeline: {
|
||||||
todaysMood: .good,
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 5)
|
||||||
stats: MoodStats(totalEntries: 45, moodCounts: [.great: 15, .good: 18, .average: 8, .bad: 3, .horrible: 1]),
|
}
|
||||||
promptText: ""
|
|
||||||
))
|
|
||||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
|
||||||
.previewDisplayName("Vote - Medium (Voted)")
|
|
||||||
|
|
||||||
// MARK: - FeelsVoteWidget (Non-Subscriber)
|
// Medium - Voting States
|
||||||
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry(
|
#Preview("Timeline Medium - Voting", as: .systemMedium) {
|
||||||
date: Date(),
|
FeelsWidget()
|
||||||
hasSubscription: false,
|
} timeline: {
|
||||||
hasVotedToday: false,
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 5, hasVotedToday: false)
|
||||||
todaysMood: nil,
|
}
|
||||||
stats: nil,
|
|
||||||
promptText: ""
|
#Preview("Timeline Medium - Non-Subscriber", as: .systemMedium) {
|
||||||
))
|
FeelsWidget()
|
||||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
} timeline: {
|
||||||
.previewDisplayName("Vote - Small (Non-Subscriber)")
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 5, hasVotedToday: false, hasSubscription: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Large - Logged States
|
||||||
|
#Preview("Timeline Large - Logged", as: .systemLarge) {
|
||||||
|
FeelsWidget()
|
||||||
|
} timeline: {
|
||||||
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Large - Voting States
|
||||||
|
#Preview("Timeline Large - Voting", as: .systemLarge) {
|
||||||
|
FeelsWidget()
|
||||||
|
} timeline: {
|
||||||
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 10, hasVotedToday: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Timeline Large - Non-Subscriber", as: .systemLarge) {
|
||||||
|
FeelsWidget()
|
||||||
|
} timeline: {
|
||||||
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 10, hasVotedToday: false, hasSubscription: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - FeelsGraphicWidget Previews (Mood Graphic)
|
||||||
|
|
||||||
|
#Preview("Graphic - Great", as: .systemSmall) {
|
||||||
|
FeelsGraphicWidget()
|
||||||
|
} timeline: {
|
||||||
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 2, startMood: .great)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Graphic - Good", as: .systemSmall) {
|
||||||
|
FeelsGraphicWidget()
|
||||||
|
} timeline: {
|
||||||
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 2, startMood: .good)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Graphic - Average", as: .systemSmall) {
|
||||||
|
FeelsGraphicWidget()
|
||||||
|
} timeline: {
|
||||||
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 2, startMood: .average)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Graphic - Bad", as: .systemSmall) {
|
||||||
|
FeelsGraphicWidget()
|
||||||
|
} timeline: {
|
||||||
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 2, startMood: .bad)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Graphic - Horrible", as: .systemSmall) {
|
||||||
|
FeelsGraphicWidget()
|
||||||
|
} timeline: {
|
||||||
|
WidgetPreviewHelpers.sampleEntry(timelineCount: 2, startMood: .horrible)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - FeelsIconWidget Previews (Custom Icon)
|
||||||
|
|
||||||
|
#Preview("Custom Icon", as: .systemSmall) {
|
||||||
|
FeelsIconWidget()
|
||||||
|
} timeline: {
|
||||||
|
WidgetPreviewHelpers.sampleEntry()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Live Activity Previews (Lock Screen View)
|
||||||
|
|
||||||
|
#Preview("Live Activity - Not Logged") {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Image(systemName: "flame.fill")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
Text("7")
|
||||||
|
.font(.title.bold())
|
||||||
|
Text("day streak")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.frame(height: 50)
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Don't break your streak!")
|
||||||
|
.font(.headline)
|
||||||
|
Text("Tap to log your mood")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemBackground).opacity(0.8))
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Live Activity - Great") {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Image(systemName: "flame.fill")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
Text("15")
|
||||||
|
.font(.title.bold())
|
||||||
|
Text("day streak")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.frame(height: 50)
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Circle()
|
||||||
|
.fill(MoodTints.Default.color(forMood: .great))
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Today's mood")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("Great")
|
||||||
|
.font(.headline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemBackground).opacity(0.8))
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Live Activity - Good") {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Image(systemName: "flame.fill")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
Text("30")
|
||||||
|
.font(.title.bold())
|
||||||
|
Text("day streak")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.frame(height: 50)
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Circle()
|
||||||
|
.fill(MoodTints.Default.color(forMood: .good))
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Today's mood")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("Good")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemBackground).opacity(0.8))
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Live Activity - Average") {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Image(systemName: "flame.fill")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
Text("10")
|
||||||
|
.font(.title.bold())
|
||||||
|
Text("day streak")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.frame(height: 50)
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Circle()
|
||||||
|
.fill(MoodTints.Default.color(forMood: .average))
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Today's mood")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("Average")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemBackground).opacity(0.8))
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Live Activity - Bad") {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Image(systemName: "flame.fill")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
Text("5")
|
||||||
|
.font(.title.bold())
|
||||||
|
Text("day streak")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.frame(height: 50)
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Circle()
|
||||||
|
.fill(MoodTints.Default.color(forMood: .bad))
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Today's mood")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("Bad")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemBackground).opacity(0.8))
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Live Activity - Horrible") {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Image(systemName: "flame.fill")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
Text("3")
|
||||||
|
.font(.title.bold())
|
||||||
|
Text("day streak")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.frame(height: 50)
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Circle()
|
||||||
|
.fill(MoodTints.Default.color(forMood: .horrible))
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Today's mood")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("Horrible")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemBackground).opacity(0.8))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ enum Mood: Int {
|
|||||||
|
|
||||||
var graphic: Image {
|
var graphic: Image {
|
||||||
switch self {
|
switch self {
|
||||||
|
|
||||||
case .horrible:
|
case .horrible:
|
||||||
return Image("HorribleGraphic", bundle: .main)
|
return Image("HorribleGraphic", bundle: .main)
|
||||||
case .bad:
|
case .bad:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ struct OnboardingCustomizeOne: View {
|
|||||||
.foregroundColor(Color(UIColor.darkText))
|
.foregroundColor(Color(UIColor.darkText))
|
||||||
.opacity(0.04)
|
.opacity(0.04)
|
||||||
.scaleEffect(1.2, anchor: .trailing)
|
.scaleEffect(1.2, anchor: .trailing)
|
||||||
|
.accessibilityHidden(true)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ struct OnboardingCustomizeTwo: View {
|
|||||||
.foregroundColor(Color(UIColor.darkText))
|
.foregroundColor(Color(UIColor.darkText))
|
||||||
.opacity(0.04)
|
.opacity(0.04)
|
||||||
.scaleEffect(1.2, anchor: .trailing)
|
.scaleEffect(1.2, anchor: .trailing)
|
||||||
|
.accessibilityHidden(true)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,21 +44,21 @@ struct OnboardingDay: View {
|
|||||||
.frame(width: 120, height: 120)
|
.frame(width: 120, height: 120)
|
||||||
|
|
||||||
Image(systemName: "calendar")
|
Image(systemName: "calendar")
|
||||||
.font(.system(size: 44))
|
.font(.largeTitle)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
.padding(.bottom, 32)
|
.padding(.bottom, 32)
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
Text("Which day should\nyou rate?")
|
Text("Which day should\nyou rate?")
|
||||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
.font(.title.weight(.bold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.bottom, 12)
|
.padding(.bottom, 12)
|
||||||
|
|
||||||
// Subtitle
|
// Subtitle
|
||||||
Text("When you get your reminder, do you want to rate today or yesterday?")
|
Text("When you get your reminder, do you want to rate today or yesterday?")
|
||||||
.font(.system(size: 16, weight: .medium))
|
.font(.body.weight(.medium))
|
||||||
.foregroundColor(.white.opacity(0.85))
|
.foregroundColor(.white.opacity(0.85))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, 40)
|
.padding(.horizontal, 40)
|
||||||
@@ -92,11 +92,11 @@ struct OnboardingDay: View {
|
|||||||
// Tip
|
// Tip
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Image(systemName: "lightbulb.fill")
|
Image(systemName: "lightbulb.fill")
|
||||||
.font(.system(size: 18))
|
.font(.headline)
|
||||||
.foregroundColor(.yellow)
|
.foregroundColor(.yellow)
|
||||||
|
|
||||||
Text("Tip: \"Yesterday\" works great for evening reminders")
|
Text("Tip: \"Yesterday\" works great for evening reminders")
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(.white.opacity(0.9))
|
.foregroundColor(.white.opacity(0.9))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 30)
|
.padding(.horizontal, 30)
|
||||||
@@ -124,7 +124,7 @@ struct DayOptionCard: View {
|
|||||||
.frame(width: 46, height: 46)
|
.frame(width: 46, height: 46)
|
||||||
|
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.system(size: 20))
|
.font(.title3)
|
||||||
.foregroundColor(isSelected ? Color(hex: "4facfe") : .white)
|
.foregroundColor(isSelected ? Color(hex: "4facfe") : .white)
|
||||||
}
|
}
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
@@ -132,15 +132,15 @@ struct DayOptionCard: View {
|
|||||||
// Text
|
// Text
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: 17, weight: .semibold))
|
.font(.body.weight(.semibold))
|
||||||
.foregroundColor(isSelected ? Color(hex: "4facfe") : .white)
|
.foregroundColor(isSelected ? Color(hex: "4facfe") : .white)
|
||||||
|
|
||||||
Text(subtitle)
|
Text(subtitle)
|
||||||
.font(.system(size: 13))
|
.font(.caption)
|
||||||
.foregroundColor(isSelected ? Color(hex: "4facfe").opacity(0.8) : .white.opacity(0.8))
|
.foregroundColor(isSelected ? Color(hex: "4facfe").opacity(0.8) : .white.opacity(0.8))
|
||||||
|
|
||||||
Text(example)
|
Text(example)
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.caption2.weight(.medium))
|
||||||
.foregroundColor(isSelected ? Color(hex: "4facfe").opacity(0.6) : .white.opacity(0.6))
|
.foregroundColor(isSelected ? Color(hex: "4facfe").opacity(0.6) : .white.opacity(0.6))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.minimumScaleFactor(0.8)
|
.minimumScaleFactor(0.8)
|
||||||
@@ -151,7 +151,7 @@ struct DayOptionCard: View {
|
|||||||
// Checkmark
|
// Checkmark
|
||||||
if isSelected {
|
if isSelected {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.font(.system(size: 22))
|
.font(.title3)
|
||||||
.foregroundColor(Color(hex: "4facfe"))
|
.foregroundColor(Color(hex: "4facfe"))
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct OnboardingStyle: View {
|
|||||||
.frame(width: 100, height: 100)
|
.frame(width: 100, height: 100)
|
||||||
|
|
||||||
Image(systemName: "paintpalette.fill")
|
Image(systemName: "paintpalette.fill")
|
||||||
.font(.system(size: 40))
|
.font(.largeTitle)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
.padding(.top, 40)
|
.padding(.top, 40)
|
||||||
@@ -39,13 +39,13 @@ struct OnboardingStyle: View {
|
|||||||
|
|
||||||
// Title
|
// Title
|
||||||
Text("Make it yours")
|
Text("Make it yours")
|
||||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
.font(.title.weight(.bold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
|
|
||||||
// Subtitle
|
// Subtitle
|
||||||
Text("Choose your favorite style")
|
Text("Choose your favorite style")
|
||||||
.font(.system(size: 16, weight: .medium))
|
.font(.body.weight(.medium))
|
||||||
.foregroundColor(.white.opacity(0.85))
|
.foregroundColor(.white.opacity(0.85))
|
||||||
.padding(.bottom, 20)
|
.padding(.bottom, 20)
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ struct OnboardingStyle: View {
|
|||||||
// Icon Style Section
|
// Icon Style Section
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text("Icon Style")
|
Text("Icon Style")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(.white.opacity(0.9))
|
.foregroundColor(.white.opacity(0.9))
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ struct OnboardingStyle: View {
|
|||||||
// Color Theme Section
|
// Color Theme Section
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text("Mood Colors")
|
Text("Mood Colors")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(.white.opacity(0.9))
|
.foregroundColor(.white.opacity(0.9))
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
|
|
||||||
@@ -105,9 +105,9 @@ struct OnboardingStyle: View {
|
|||||||
// Hint
|
// Hint
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Image(systemName: "arrow.left.arrow.right")
|
Image(systemName: "arrow.left.arrow.right")
|
||||||
.font(.system(size: 14))
|
.font(.subheadline)
|
||||||
Text("You can change these anytime in Customize")
|
Text("You can change these anytime in Customize")
|
||||||
.font(.system(size: 13, weight: .medium))
|
.font(.caption.weight(.medium))
|
||||||
}
|
}
|
||||||
.foregroundColor(.white.opacity(0.7))
|
.foregroundColor(.white.opacity(0.7))
|
||||||
.padding(.top, 20)
|
.padding(.top, 20)
|
||||||
@@ -130,14 +130,15 @@ struct OnboardingStylePreview: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 44, height: 44)
|
.frame(width: 44, height: 44)
|
||||||
.foregroundColor(moodTint.color(forMood: .good))
|
.foregroundColor(moodTint.color(forMood: .good))
|
||||||
|
.accessibilityLabel(Mood.good.strValue)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Wednesday - 10th")
|
Text("Wednesday - 10th")
|
||||||
.font(.system(size: 17, weight: .semibold))
|
.font(.body.weight(.semibold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|
||||||
Text(Mood.good.strValue)
|
Text(Mood.good.strValue)
|
||||||
.font(.system(size: 14))
|
.font(.subheadline)
|
||||||
.foregroundColor(.white.opacity(0.8))
|
.foregroundColor(.white.opacity(0.8))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +168,7 @@ struct OnboardingIconPackOption: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 24, height: 24)
|
.frame(width: 24, height: 24)
|
||||||
.foregroundColor(moodTint.color(forMood: mood))
|
.foregroundColor(moodTint.color(forMood: mood))
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
|
|||||||
@@ -34,14 +34,14 @@ struct OnboardingSubscription: View {
|
|||||||
.frame(width: 120, height: 120)
|
.frame(width: 120, height: 120)
|
||||||
|
|
||||||
Image(systemName: "crown.fill")
|
Image(systemName: "crown.fill")
|
||||||
.font(.system(size: 48))
|
.font(.largeTitle)
|
||||||
.foregroundColor(.yellow)
|
.foregroundColor(.yellow)
|
||||||
}
|
}
|
||||||
.padding(.bottom, 24)
|
.padding(.bottom, 24)
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
Text("Unlock the Full\nExperience")
|
Text("Unlock the Full\nExperience")
|
||||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
.font(.title.weight(.bold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
@@ -101,10 +101,10 @@ struct OnboardingSubscription: View {
|
|||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "sparkles")
|
Image(systemName: "sparkles")
|
||||||
.font(.system(size: 18, weight: .semibold))
|
.font(.headline.weight(.semibold))
|
||||||
|
|
||||||
Text("Get Personal Insights")
|
Text("Get Personal Insights")
|
||||||
.font(.system(size: 18, weight: .bold))
|
.font(.headline.weight(.bold))
|
||||||
}
|
}
|
||||||
.foregroundColor(Color(hex: "11998e"))
|
.foregroundColor(Color(hex: "11998e"))
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
@@ -125,7 +125,7 @@ struct OnboardingSubscription: View {
|
|||||||
completionClosure(onboardingData)
|
completionClosure(onboardingData)
|
||||||
}) {
|
}) {
|
||||||
Text("Maybe Later")
|
Text("Maybe Later")
|
||||||
.font(.system(size: 16, weight: .medium))
|
.font(.body.weight(.medium))
|
||||||
.foregroundColor(.white.opacity(0.8))
|
.foregroundColor(.white.opacity(0.8))
|
||||||
}
|
}
|
||||||
.accessibilityLabel(String(localized: "Maybe Later"))
|
.accessibilityLabel(String(localized: "Maybe Later"))
|
||||||
@@ -154,18 +154,18 @@ struct BenefitRow: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.system(size: 22))
|
.font(.title3)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.frame(width: 40)
|
.frame(width: 40)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.font(.body.weight(.semibold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|
||||||
Text(description)
|
Text(description)
|
||||||
.font(.system(size: 13))
|
.font(.caption)
|
||||||
.foregroundColor(.white.opacity(0.8))
|
.foregroundColor(.white.opacity(0.8))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,21 +36,21 @@ struct OnboardingTime: View {
|
|||||||
.frame(width: 120, height: 120)
|
.frame(width: 120, height: 120)
|
||||||
|
|
||||||
Image(systemName: "bell.fill")
|
Image(systemName: "bell.fill")
|
||||||
.font(.system(size: 44))
|
.font(.largeTitle)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
.padding(.bottom, 32)
|
.padding(.bottom, 32)
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
Text("When should we\nremind you?")
|
Text("When should we\nremind you?")
|
||||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
.font(.title.weight(.bold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.bottom, 12)
|
.padding(.bottom, 12)
|
||||||
|
|
||||||
// Subtitle
|
// Subtitle
|
||||||
Text("Pick a time that works for your daily check-in")
|
Text("Pick a time that works for your daily check-in")
|
||||||
.font(.system(size: 16, weight: .medium))
|
.font(.body.weight(.medium))
|
||||||
.foregroundColor(.white.opacity(0.85))
|
.foregroundColor(.white.opacity(0.85))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, 40)
|
.padding(.horizontal, 40)
|
||||||
@@ -80,12 +80,12 @@ struct OnboardingTime: View {
|
|||||||
// Info text
|
// Info text
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Image(systemName: "info.circle.fill")
|
Image(systemName: "info.circle.fill")
|
||||||
.font(.system(size: 20))
|
.font(.title3)
|
||||||
.foregroundColor(.white.opacity(0.8))
|
.foregroundColor(.white.opacity(0.8))
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
|
|
||||||
Text("You'll get a gentle reminder at \(formatter.string(from: onboardingData.date)) every day")
|
Text("You'll get a gentle reminder at \(formatter.string(from: onboardingData.date)) every day")
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(.white.opacity(0.9))
|
.foregroundColor(.white.opacity(0.9))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 30)
|
.padding(.horizontal, 30)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ struct OnboardingTitle: View {
|
|||||||
.opacity(0.04)
|
.opacity(0.04)
|
||||||
.scaleEffect(1.2)
|
.scaleEffect(1.2)
|
||||||
.padding(.bottom, 55)
|
.padding(.bottom, 55)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack{
|
VStack{
|
||||||
@@ -37,8 +38,7 @@ struct OnboardingTitle: View {
|
|||||||
// onboardingData.title = option
|
// onboardingData.title = option
|
||||||
}, label: {
|
}, label: {
|
||||||
Text(option)
|
Text(option)
|
||||||
.font(.system(size: 15))
|
.font(.subheadline.weight(.bold))
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.background(RoundedRectangle(cornerRadius: 10).stroke().foregroundColor(Color.white))
|
.background(RoundedRectangle(cornerRadius: 10).stroke().foregroundColor(Color.white))
|
||||||
|
|||||||
@@ -32,20 +32,20 @@ struct OnboardingWelcome: View {
|
|||||||
.frame(width: 120, height: 120)
|
.frame(width: 120, height: 120)
|
||||||
|
|
||||||
Image(systemName: "heart.fill")
|
Image(systemName: "heart.fill")
|
||||||
.font(.system(size: 50))
|
.font(.largeTitle)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
.padding(.bottom, 40)
|
.padding(.bottom, 40)
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
Text("Welcome to Feels")
|
Text("Welcome to Feels")
|
||||||
.font(.system(size: 34, weight: .bold, design: .rounded))
|
.font(.largeTitle.weight(.bold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(.bottom, 12)
|
.padding(.bottom, 12)
|
||||||
|
|
||||||
// Subtitle
|
// Subtitle
|
||||||
Text("Track your mood, discover patterns,\nand understand yourself better.")
|
Text("Track your mood, discover patterns,\nand understand yourself better.")
|
||||||
.font(.system(size: 18, weight: .medium))
|
.font(.headline.weight(.medium))
|
||||||
.foregroundColor(.white.opacity(0.9))
|
.foregroundColor(.white.opacity(0.9))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, 40)
|
.padding(.horizontal, 40)
|
||||||
@@ -91,18 +91,18 @@ struct FeatureRow: View {
|
|||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
|
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.system(size: 22))
|
.font(.title3)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.font(.body.weight(.semibold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|
||||||
Text(description)
|
Text(description)
|
||||||
.font(.system(size: 14))
|
.font(.subheadline)
|
||||||
.foregroundColor(.white.opacity(0.8))
|
.foregroundColor(.white.opacity(0.8))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ struct OnboardingWrapup: View {
|
|||||||
.foregroundColor(Color(UIColor.darkText))
|
.foregroundColor(Color(UIColor.darkText))
|
||||||
.opacity(0.04)
|
.opacity(0.04)
|
||||||
.scaleEffect(1.2, anchor: .trailing)
|
.scaleEffect(1.2, anchor: .trailing)
|
||||||
|
.accessibilityHidden(true)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,11 +90,6 @@ extension Font {
|
|||||||
static func scalable(_ style: Font.TextStyle, weight: Font.Weight = .regular) -> Font {
|
static func scalable(_ style: Font.TextStyle, weight: Font.Weight = .regular) -> Font {
|
||||||
Font.system(style, design: .rounded).weight(weight)
|
Font.system(style, design: .rounded).weight(weight)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a custom-sized font that scales with Dynamic Type
|
|
||||||
static func scaledSystem(size: CGFloat, weight: Font.Weight = .regular, design: Font.Design = .default, relativeTo style: Font.TextStyle = .body) -> Font {
|
|
||||||
Font.system(size: size, weight: weight, design: design)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Property wrapper for scaled metrics that respect Dynamic Type
|
/// Property wrapper for scaled metrics that respect Dynamic Type
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ struct AuraVotingView: View {
|
|||||||
|
|
||||||
// Label with elegant typography
|
// Label with elegant typography
|
||||||
Text(mood.strValue)
|
Text(mood.strValue)
|
||||||
.font(.system(size: 12, weight: .semibold, design: .rounded))
|
.font(.caption.weight(.semibold))
|
||||||
.foregroundColor(color)
|
.foregroundColor(color)
|
||||||
.tracking(0.5)
|
.tracking(0.5)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ struct BGViewItem: View {
|
|||||||
.foregroundColor(DefaultMoodTint.color(forMood: mood))
|
.foregroundColor(DefaultMoodTint.color(forMood: mood))
|
||||||
// .blur(radius: 3)
|
// .blur(radius: 3)
|
||||||
.opacity(0.1)
|
.opacity(0.1)
|
||||||
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ struct CustomizeView: View {
|
|||||||
private var headerView: some View {
|
private var headerView: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Customize")
|
Text("Customize")
|
||||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
.font(.title.weight(.bold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -214,7 +214,7 @@ struct SettingsSection<Content: View>: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Text(title.uppercased())
|
Text(title.uppercased())
|
||||||
.font(.system(size: 13, weight: .semibold))
|
.font(.caption.weight(.semibold))
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
.tracking(0.5)
|
.tracking(0.5)
|
||||||
|
|
||||||
@@ -240,7 +240,7 @@ struct SettingsRow<Content: View>: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: 15, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.7))
|
.foregroundColor(textColor.opacity(0.7))
|
||||||
|
|
||||||
content
|
content
|
||||||
@@ -272,7 +272,7 @@ struct ThemePickerCompact: View {
|
|||||||
|
|
||||||
if theme == aTheme {
|
if theme == aTheme {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.font(.system(size: 18))
|
.font(.headline)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
.background(Circle().fill(.white).padding(2))
|
.background(Circle().fill(.white).padding(2))
|
||||||
.offset(x: 14, y: 14)
|
.offset(x: 14, y: 14)
|
||||||
@@ -280,7 +280,7 @@ struct ThemePickerCompact: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(aTheme.title)
|
Text(aTheme.title)
|
||||||
.font(.system(size: 12, weight: .medium))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(theme == aTheme ? .accentColor : textColor.opacity(0.6))
|
.foregroundColor(theme == aTheme ? .accentColor : textColor.opacity(0.6))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,7 +310,7 @@ struct TextColorPickerCompact: View {
|
|||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
|
|
||||||
Text("Sample Text")
|
Text("Sample Text")
|
||||||
.font(.system(size: 16, weight: .medium))
|
.font(.body.weight(.medium))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -344,6 +344,7 @@ struct ImagePackPickerCompact: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 28, height: 28)
|
.frame(width: 28, height: 28)
|
||||||
.foregroundColor(moodTint.color(forMood: mood))
|
.foregroundColor(moodTint.color(forMood: mood))
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +352,7 @@ struct ImagePackPickerCompact: View {
|
|||||||
|
|
||||||
if imagePack == images {
|
if imagePack == images {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.font(.system(size: 22))
|
.font(.title2)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,7 +399,7 @@ struct TintPickerCompact: View {
|
|||||||
|
|
||||||
if moodTint == tint {
|
if moodTint == tint {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.font(.system(size: 22))
|
.font(.title2)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,11 +433,11 @@ struct TintPickerCompact: View {
|
|||||||
|
|
||||||
if moodTint == .Custom {
|
if moodTint == .Custom {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.font(.system(size: 22))
|
.font(.title2)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
} else {
|
} else {
|
||||||
Text("Custom")
|
Text("Custom")
|
||||||
.font(.system(size: 13))
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -485,9 +486,13 @@ struct VotingLayoutPickerCompact: View {
|
|||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
|
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
if UIAccessibility.isReduceMotionEnabled {
|
||||||
|
votingLayoutStyle = layout.rawValue
|
||||||
|
} else {
|
||||||
withAnimation(.easeInOut(duration: 0.2)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
votingLayoutStyle = layout.rawValue
|
votingLayoutStyle = layout.rawValue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
|
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
|
||||||
}) {
|
}) {
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
@@ -496,7 +501,7 @@ struct VotingLayoutPickerCompact: View {
|
|||||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.4))
|
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.4))
|
||||||
|
|
||||||
Text(layout.displayName)
|
Text(layout.displayName)
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.caption2.weight(.medium))
|
||||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.5))
|
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.5))
|
||||||
}
|
}
|
||||||
.frame(width: 70)
|
.frame(width: 70)
|
||||||
@@ -608,7 +613,7 @@ struct CustomWidgetSection: View {
|
|||||||
.frame(width: 60, height: 60)
|
.frame(width: 60, height: 60)
|
||||||
|
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
.font(.system(size: 24, weight: .medium))
|
.font(.title2.weight(.medium))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -618,9 +623,9 @@ struct CustomWidgetSection: View {
|
|||||||
Link(destination: URL(string: "https://support.apple.com/guide/iphone/add-widgets-iphb8f1bf206/ios")!) {
|
Link(destination: URL(string: "https://support.apple.com/guide/iphone/add-widgets-iphb8f1bf206/ios")!) {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Image(systemName: "questionmark.circle")
|
Image(systemName: "questionmark.circle")
|
||||||
.font(.system(size: 14))
|
.font(.subheadline)
|
||||||
Text("How to add widgets")
|
Text("How to add widgets")
|
||||||
.font(.system(size: 14))
|
.font(.subheadline)
|
||||||
}
|
}
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
}
|
}
|
||||||
@@ -659,12 +664,12 @@ struct PersonalityPackPickerCompact: View {
|
|||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(String(aPack.title()))
|
Text(String(aPack.title()))
|
||||||
.font(.system(size: 15, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
let strings = aPack.randomPushNotificationStrings()
|
let strings = aPack.randomPushNotificationStrings()
|
||||||
Text(strings.body)
|
Text(strings.body)
|
||||||
.font(.system(size: 13))
|
.font(.caption)
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
}
|
}
|
||||||
@@ -673,7 +678,7 @@ struct PersonalityPackPickerCompact: View {
|
|||||||
|
|
||||||
if personalityPack == aPack {
|
if personalityPack == aPack {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.font(.system(size: 22))
|
.font(.title2)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -736,7 +741,7 @@ struct DayFilterPickerCompact: View {
|
|||||||
impactMed.impactOccurred()
|
impactMed.impactOccurred()
|
||||||
}) {
|
}) {
|
||||||
Text(day.prefix(2).uppercased())
|
Text(day.prefix(2).uppercased())
|
||||||
.font(.system(size: 13, weight: .semibold))
|
.font(.caption.weight(.semibold))
|
||||||
.foregroundColor(isActive ? .white : textColor.opacity(0.5))
|
.foregroundColor(isActive ? .white : textColor.opacity(0.5))
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.frame(height: 40)
|
.frame(height: 40)
|
||||||
@@ -750,7 +755,7 @@ struct DayFilterPickerCompact: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(String(localized: "day_picker_view_text"))
|
Text(String(localized: "day_picker_view_text"))
|
||||||
.font(.system(size: 13))
|
.font(.caption)
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
}
|
}
|
||||||
@@ -774,15 +779,15 @@ struct SubscriptionBannerView: View {
|
|||||||
private var subscribedView: some View {
|
private var subscribedView: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Image(systemName: "checkmark.seal.fill")
|
Image(systemName: "checkmark.seal.fill")
|
||||||
.font(.system(size: 28))
|
.font(.title)
|
||||||
.foregroundColor(.green)
|
.foregroundColor(.green)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Premium Active")
|
Text("Premium Active")
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.font(.body.weight(.semibold))
|
||||||
|
|
||||||
Text("You have full access")
|
Text("You have full access")
|
||||||
.font(.system(size: 13))
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,7 +798,7 @@ struct SubscriptionBannerView: View {
|
|||||||
await openSubscriptionManagement()
|
await openSubscriptionManagement()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(.green)
|
.foregroundColor(.green)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
@@ -813,23 +818,23 @@ struct SubscriptionBannerView: View {
|
|||||||
}) {
|
}) {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Image(systemName: "crown.fill")
|
Image(systemName: "crown.fill")
|
||||||
.font(.system(size: 28))
|
.font(.title)
|
||||||
.foregroundColor(.orange)
|
.foregroundColor(.orange)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Unlock Premium")
|
Text("Unlock Premium")
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.font(.body.weight(.semibold))
|
||||||
.foregroundColor(colorScheme == .dark ? .white : .black)
|
.foregroundColor(colorScheme == .dark ? .white : .black)
|
||||||
|
|
||||||
Text("Month & Year views, Insights & more")
|
Text("Month & Year views, Insights & more")
|
||||||
.font(.system(size: 13))
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
@@ -870,9 +875,13 @@ struct DayViewStylePickerCompact: View {
|
|||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
ForEach(DayViewStyle.allCases, id: \.rawValue) { style in
|
ForEach(DayViewStyle.allCases, id: \.rawValue) { style in
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
if UIAccessibility.isReduceMotionEnabled {
|
||||||
|
dayViewStyle = style
|
||||||
|
} else {
|
||||||
withAnimation(.easeInOut(duration: 0.2)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
dayViewStyle = style
|
dayViewStyle = style
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
||||||
impactMed.impactOccurred()
|
impactMed.impactOccurred()
|
||||||
EventLogger.log(event: "change_day_view_style", withData: ["style": style.displayName])
|
EventLogger.log(event: "change_day_view_style", withData: ["style": style.displayName])
|
||||||
@@ -883,7 +892,7 @@ struct DayViewStylePickerCompact: View {
|
|||||||
.foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.4))
|
.foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.4))
|
||||||
|
|
||||||
Text(style.displayName)
|
Text(style.displayName)
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.caption2.weight(.medium))
|
||||||
.foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.5))
|
.foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.5))
|
||||||
}
|
}
|
||||||
.frame(width: 70)
|
.frame(width: 70)
|
||||||
@@ -962,7 +971,7 @@ struct DayViewStylePickerCompact: View {
|
|||||||
// Giant number with glowing orb
|
// Giant number with glowing orb
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Text("17")
|
Text("17")
|
||||||
.font(.system(size: 20, weight: .black, design: .rounded))
|
.font(.title3.weight(.black))
|
||||||
.foregroundStyle(
|
.foregroundStyle(
|
||||||
LinearGradient(colors: [.green, .green.opacity(0.5)], startPoint: .top, endPoint: .bottom)
|
LinearGradient(colors: [.green, .green.opacity(0.5)], startPoint: .top, endPoint: .bottom)
|
||||||
)
|
)
|
||||||
@@ -983,7 +992,7 @@ struct DayViewStylePickerCompact: View {
|
|||||||
Rectangle().frame(width: 34, height: 2)
|
Rectangle().frame(width: 34, height: 2)
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Text("12")
|
Text("12")
|
||||||
.font(.system(size: 18, weight: .regular, design: .serif))
|
.font(.headline.weight(.regular))
|
||||||
Rectangle().frame(width: 1, height: 20)
|
Rectangle().frame(width: 1, height: 20)
|
||||||
VStack(alignment: .leading, spacing: 1) {
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
RoundedRectangle(cornerRadius: 1).frame(width: 12, height: 3)
|
RoundedRectangle(cornerRadius: 1).frame(width: 12, height: 3)
|
||||||
@@ -1176,7 +1185,7 @@ struct DayViewStylePickerCompact: View {
|
|||||||
.offset(x: -6, y: 4)
|
.offset(x: -6, y: 4)
|
||||||
.blur(radius: 2)
|
.blur(radius: 2)
|
||||||
Image(systemName: "gyroscope")
|
Image(systemName: "gyroscope")
|
||||||
.font(.system(size: 12, weight: .medium))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
case .micro:
|
case .micro:
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ struct IconPickerView: View {
|
|||||||
.frame(width: 50, height:50)
|
.frame(width: 50, height:50)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
})
|
})
|
||||||
|
.accessibilityLabel(String(localized: "Default app icon"))
|
||||||
|
.accessibilityHint(String(localized: "Double tap to select"))
|
||||||
|
|
||||||
|
|
||||||
ForEach(iconSets, id: \.self.0){ iconSet in
|
ForEach(iconSets, id: \.self.0){ iconSet in
|
||||||
@@ -78,6 +80,8 @@ struct IconPickerView: View {
|
|||||||
.frame(width: 50, height:50)
|
.frame(width: 50, height:50)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
})
|
})
|
||||||
|
.accessibilityLabel(String(localized: "App icon style \(iconSet.1.replacingOccurrences(of: "AppIcon", with: "").replacingOccurrences(of: "Image", with: ""))"))
|
||||||
|
.accessibilityHint(String(localized: "Double tap to select"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ struct ImagePackPickerView: View {
|
|||||||
.foregroundColor(
|
.foregroundColor(
|
||||||
moodTint.color(forMood: mood)
|
moodTint.color(forMood: mood)
|
||||||
)
|
)
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
}
|
}
|
||||||
.frame(minWidth: 0, maxWidth: .infinity)
|
.frame(minWidth: 0, maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,13 @@ struct VotingLayoutPickerView: View {
|
|||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
|
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
if UIAccessibility.isReduceMotionEnabled {
|
||||||
|
votingLayoutStyle = layout.rawValue
|
||||||
|
} else {
|
||||||
withAnimation(.easeInOut(duration: 0.2)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
votingLayoutStyle = layout.rawValue
|
votingLayoutStyle = layout.rawValue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
|
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
|
||||||
}) {
|
}) {
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
|
|||||||
@@ -172,12 +172,12 @@ extension DayView {
|
|||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
// Calendar icon
|
// Calendar icon
|
||||||
Image(systemName: "calendar")
|
Image(systemName: "calendar")
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.font(.body.weight(.semibold))
|
||||||
.foregroundColor(textColor.opacity(0.6))
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
|
|
||||||
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
||||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
.font(.title3.weight(.bold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -194,7 +194,7 @@ extension DayView {
|
|||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
// Large month number as hero element
|
// Large month number as hero element
|
||||||
Text(String(format: "%02d", month))
|
Text(String(format: "%02d", month))
|
||||||
.font(.system(size: 48, weight: .black, design: .rounded))
|
.font(.largeTitle.weight(.black))
|
||||||
.foregroundStyle(
|
.foregroundStyle(
|
||||||
LinearGradient(
|
LinearGradient(
|
||||||
colors: [textColor, textColor.opacity(0.4)],
|
colors: [textColor, textColor.opacity(0.4)],
|
||||||
@@ -206,12 +206,12 @@ extension DayView {
|
|||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(Random.monthName(fromMonthInt: month).uppercased())
|
Text(Random.monthName(fromMonthInt: month).uppercased())
|
||||||
.font(.system(size: 14, weight: .bold, design: .rounded))
|
.font(.subheadline.weight(.bold))
|
||||||
.tracking(3)
|
.tracking(3)
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Text(String(year))
|
Text(String(year))
|
||||||
.font(.system(size: 12, weight: .medium, design: .rounded))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,12 +253,12 @@ extension DayView {
|
|||||||
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||||
// Large serif month name
|
// Large serif month name
|
||||||
Text(Random.monthName(fromMonthInt: month).uppercased())
|
Text(Random.monthName(fromMonthInt: month).uppercased())
|
||||||
.font(.system(size: 28, weight: .regular, design: .serif))
|
.font(.title.weight(.regular))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
// Year in lighter weight
|
// Year in lighter weight
|
||||||
Text(String(year))
|
Text(String(year))
|
||||||
.font(.system(size: 16, weight: .light, design: .serif))
|
.font(.body.weight(.light))
|
||||||
.italic()
|
.italic()
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ extension DayView {
|
|||||||
|
|
||||||
// Decorative flourish
|
// Decorative flourish
|
||||||
Text("§")
|
Text("§")
|
||||||
.font(.system(size: 20, weight: .regular, design: .serif))
|
.font(.title3.weight(.regular))
|
||||||
.foregroundColor(textColor.opacity(0.3))
|
.foregroundColor(textColor.opacity(0.3))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
@@ -284,12 +284,12 @@ extension DayView {
|
|||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
// Glowing terminal prompt
|
// Glowing terminal prompt
|
||||||
Text(">")
|
Text(">")
|
||||||
.font(.system(size: 18, weight: .bold, design: .monospaced))
|
.font(.headline.weight(.bold).monospaced())
|
||||||
.foregroundColor(Color(red: 0.4, green: 1.0, blue: 0.4))
|
.foregroundColor(Color(red: 0.4, green: 1.0, blue: 0.4))
|
||||||
.shadow(color: Color(red: 0.4, green: 1.0, blue: 0.4).opacity(0.8), radius: 4, x: 0, y: 0)
|
.shadow(color: Color(red: 0.4, green: 1.0, blue: 0.4).opacity(0.8), radius: 4, x: 0, y: 0)
|
||||||
|
|
||||||
Text("\(Random.monthName(fromMonthInt: month).uppercased())_\(String(year))")
|
Text("\(Random.monthName(fromMonthInt: month).uppercased())_\(String(year))")
|
||||||
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
.font(.body.weight(.bold).monospaced())
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.shadow(color: .white.opacity(0.3), radius: 2, x: 0, y: 0)
|
.shadow(color: .white.opacity(0.3), radius: 2, x: 0, y: 0)
|
||||||
|
|
||||||
@@ -329,12 +329,12 @@ extension DayView {
|
|||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(Random.monthName(fromMonthInt: month))
|
Text(Random.monthName(fromMonthInt: month))
|
||||||
.font(.system(size: 18, weight: .thin))
|
.font(.headline.weight(.thin))
|
||||||
.tracking(4)
|
.tracking(4)
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Text(String(year))
|
Text(String(year))
|
||||||
.font(.system(size: 11, weight: .ultraLight))
|
.font(.caption2.weight(.ultraLight))
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,7 +371,7 @@ extension DayView {
|
|||||||
// Glass content
|
// Glass content
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Text(Random.monthName(fromMonthInt: month))
|
Text(Random.monthName(fromMonthInt: month))
|
||||||
.font(.system(size: 20, weight: .semibold))
|
.font(.title3.weight(.semibold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Capsule()
|
Capsule()
|
||||||
@@ -379,7 +379,7 @@ extension DayView {
|
|||||||
.frame(width: 4, height: 4)
|
.frame(width: 4, height: 4)
|
||||||
|
|
||||||
Text(String(year))
|
Text(String(year))
|
||||||
.font(.system(size: 16, weight: .medium))
|
.font(.body.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.6))
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -405,11 +405,11 @@ extension DayView {
|
|||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("SIDE A")
|
Text("SIDE A")
|
||||||
.font(.system(size: 10, weight: .bold, design: .monospaced))
|
.font(.caption2.weight(.bold).monospaced())
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
|
|
||||||
Text("\(Random.monthName(fromMonthInt: month).uppercased()) '\(String(year).suffix(2))")
|
Text("\(Random.monthName(fromMonthInt: month).uppercased()) '\(String(year).suffix(2))")
|
||||||
.font(.system(size: 16, weight: .black, design: .rounded))
|
.font(.body.weight(.black))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
.tracking(1)
|
.tracking(1)
|
||||||
}
|
}
|
||||||
@@ -418,7 +418,7 @@ extension DayView {
|
|||||||
|
|
||||||
// Track counter
|
// Track counter
|
||||||
Text(String(format: "%02d", month))
|
Text(String(format: "%02d", month))
|
||||||
.font(.system(size: 20, weight: .bold, design: .monospaced))
|
.font(.title3.weight(.bold).monospaced())
|
||||||
.foregroundColor(textColor.opacity(0.3))
|
.foregroundColor(textColor.opacity(0.3))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
@@ -443,11 +443,11 @@ extension DayView {
|
|||||||
|
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
Text(Random.monthName(fromMonthInt: month))
|
Text(Random.monthName(fromMonthInt: month))
|
||||||
.font(.system(size: 22, weight: .light))
|
.font(.title2.weight(.light))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Text(String(year))
|
Text(String(year))
|
||||||
.font(.system(size: 14, weight: .regular))
|
.font(.subheadline.weight(.regular))
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -483,11 +483,11 @@ extension DayView {
|
|||||||
.frame(width: 2)
|
.frame(width: 2)
|
||||||
|
|
||||||
Text(Random.monthName(fromMonthInt: month))
|
Text(Random.monthName(fromMonthInt: month))
|
||||||
.font(.system(size: 18, weight: .regular, design: .serif))
|
.font(.headline.weight(.regular))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Text(String(year))
|
Text(String(year))
|
||||||
.font(.system(size: 14, weight: .light, design: .serif))
|
.font(.subheadline.weight(.light))
|
||||||
.italic()
|
.italic()
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
|
|
||||||
@@ -524,7 +524,7 @@ extension DayView {
|
|||||||
return HStack(spacing: 0) {
|
return HStack(spacing: 0) {
|
||||||
// Month number
|
// Month number
|
||||||
Text(String(format: "%02d", month))
|
Text(String(format: "%02d", month))
|
||||||
.font(.system(size: 32, weight: .thin))
|
.font(.title.weight(.thin))
|
||||||
.foregroundColor(hasData ? barColor.opacity(0.6) : textColor.opacity(0.3))
|
.foregroundColor(hasData ? barColor.opacity(0.6) : textColor.opacity(0.3))
|
||||||
.frame(width: 50)
|
.frame(width: 50)
|
||||||
|
|
||||||
@@ -554,16 +554,16 @@ extension DayView {
|
|||||||
|
|
||||||
VStack(alignment: .trailing, spacing: 2) {
|
VStack(alignment: .trailing, spacing: 2) {
|
||||||
Text(Random.monthName(fromMonthInt: month))
|
Text(Random.monthName(fromMonthInt: month))
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
if hasData {
|
if hasData {
|
||||||
Text(String(format: "%.1f avg", averageMood + 1)) // Display as 1-5
|
Text(String(format: "%.1f avg", averageMood + 1)) // Display as 1-5
|
||||||
.font(.system(size: 10, weight: .semibold))
|
.font(.caption2.weight(.semibold))
|
||||||
.foregroundColor(barColor)
|
.foregroundColor(barColor)
|
||||||
} else {
|
} else {
|
||||||
Text(String(year))
|
Text(String(year))
|
||||||
.font(.system(size: 11, weight: .regular))
|
.font(.caption2.weight(.regular))
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -576,11 +576,11 @@ extension DayView {
|
|||||||
private func patternSectionHeader(month: Int, year: Int) -> some View {
|
private func patternSectionHeader(month: Int, year: Int) -> some View {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
Image(systemName: "calendar")
|
Image(systemName: "calendar")
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.font(.body.weight(.semibold))
|
||||||
.foregroundColor(textColor.opacity(0.6))
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
|
|
||||||
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
||||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
.font(.title3.weight(.bold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -630,12 +630,12 @@ extension DayView {
|
|||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(Random.monthName(fromMonthInt: month).uppercased())
|
Text(Random.monthName(fromMonthInt: month).uppercased())
|
||||||
.font(.system(size: 16, weight: .bold, design: .serif))
|
.font(.body.weight(.bold))
|
||||||
.foregroundColor(Color(red: 0.9, green: 0.85, blue: 0.75))
|
.foregroundColor(Color(red: 0.9, green: 0.85, blue: 0.75))
|
||||||
.shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1)
|
.shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1)
|
||||||
|
|
||||||
Text(String(year))
|
Text(String(year))
|
||||||
.font(.system(size: 12, weight: .medium, design: .serif))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(Color(red: 0.8, green: 0.7, blue: 0.55))
|
.foregroundColor(Color(red: 0.8, green: 0.7, blue: 0.55))
|
||||||
}
|
}
|
||||||
.padding(.leading, 12)
|
.padding(.leading, 12)
|
||||||
@@ -726,17 +726,17 @@ extension DayView {
|
|||||||
.blur(radius: 4)
|
.blur(radius: 4)
|
||||||
|
|
||||||
Text(String(format: "%02d", month))
|
Text(String(format: "%02d", month))
|
||||||
.font(.system(size: 14, weight: .semibold, design: .rounded))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(Random.monthName(fromMonthInt: month))
|
Text(Random.monthName(fromMonthInt: month))
|
||||||
.font(.system(size: 18, weight: .medium))
|
.font(.headline.weight(.medium))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Text(String(year))
|
Text(String(year))
|
||||||
.font(.system(size: 12, weight: .regular))
|
.font(.caption.weight(.regular))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -811,18 +811,18 @@ extension DayView {
|
|||||||
.frame(width: 44, height: 44)
|
.frame(width: 44, height: 44)
|
||||||
|
|
||||||
Image(systemName: "gyroscope")
|
Image(systemName: "gyroscope")
|
||||||
.font(.system(size: 22, weight: .medium))
|
.font(.title2.weight(.medium))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
.shadow(color: Color.purple.opacity(0.3), radius: 8, x: 0, y: 4)
|
.shadow(color: Color.purple.opacity(0.3), radius: 8, x: 0, y: 4)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(Random.monthName(fromMonthInt: month))
|
Text(Random.monthName(fromMonthInt: month))
|
||||||
.font(.system(size: 20, weight: .semibold))
|
.font(.title3.weight(.semibold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Text(String(year))
|
Text(String(year))
|
||||||
.font(.system(size: 13, weight: .medium))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -830,7 +830,7 @@ extension DayView {
|
|||||||
|
|
||||||
// Tilt indicator
|
// Tilt indicator
|
||||||
Image(systemName: "iphone.gen3.radiowaves.left.and.right")
|
Image(systemName: "iphone.gen3.radiowaves.left.and.right")
|
||||||
.font(.system(size: 18))
|
.font(.headline)
|
||||||
.foregroundColor(textColor.opacity(0.3))
|
.foregroundColor(textColor.opacity(0.3))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 18)
|
.padding(.horizontal, 18)
|
||||||
@@ -852,15 +852,15 @@ extension DayView {
|
|||||||
.frame(width: 3, height: 16)
|
.frame(width: 3, height: 16)
|
||||||
|
|
||||||
Text("\(Random.monthName(fromMonthInt: month).prefix(3).uppercased())")
|
Text("\(Random.monthName(fromMonthInt: month).prefix(3).uppercased())")
|
||||||
.font(.system(size: 11, weight: .bold, design: .monospaced))
|
.font(.caption2.weight(.bold).monospaced())
|
||||||
.foregroundColor(textColor.opacity(0.6))
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
|
|
||||||
Text("•")
|
Text("•")
|
||||||
.font(.system(size: 8))
|
.font(.caption2)
|
||||||
.foregroundColor(textColor.opacity(0.3))
|
.foregroundColor(textColor.opacity(0.3))
|
||||||
|
|
||||||
Text(String(year))
|
Text(String(year))
|
||||||
.font(.system(size: 11, weight: .medium, design: .monospaced))
|
.font(.caption2.weight(.medium).monospaced())
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
|
|
||||||
// Thin separator line
|
// Thin separator line
|
||||||
|
|||||||
@@ -108,30 +108,31 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 32, height: 32)
|
.frame(width: 32, height: 32)
|
||||||
.foregroundColor(isMissing ? .gray : .white)
|
.foregroundColor(isMissing ? .gray : .white)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
}
|
}
|
||||||
.shadow(color: isMissing ? .clear : moodColor.opacity(0.4), radius: 8, x: 0, y: 4)
|
.shadow(color: isMissing ? .clear : moodColor.opacity(0.4), radius: 8, x: 0, y: 4)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text(Random.weekdayName(fromDate: entry.forDate))
|
Text(Random.weekdayName(fromDate: entry.forDate))
|
||||||
.font(.system(size: 17, weight: .semibold))
|
.font(.body.weight(.semibold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Text("•")
|
Text("•")
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
|
|
||||||
Text(Random.dayFormat(fromDate: entry.forDate))
|
Text(Random.dayFormat(fromDate: entry.forDate))
|
||||||
.font(.system(size: 17, weight: .medium))
|
.font(.body.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.8))
|
.foregroundColor(textColor.opacity(0.8))
|
||||||
}
|
}
|
||||||
|
|
||||||
if isMissing {
|
if isMissing {
|
||||||
Text(String(localized: "mood_value_missing_tap_to_add"))
|
Text(String(localized: "mood_value_missing_tap_to_add"))
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
} else {
|
} else {
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
@@ -145,7 +146,7 @@ struct EntryListView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(textColor.opacity(0.3))
|
.foregroundColor(textColor.opacity(0.3))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 18)
|
.padding(.horizontal, 18)
|
||||||
@@ -184,20 +185,21 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 22, height: 22)
|
.frame(width: 22, height: 22)
|
||||||
.foregroundColor(isMissing ? .gray : moodColor)
|
.foregroundColor(isMissing ? .gray : moodColor)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
)
|
)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.wide).day())
|
Text(entry.forDate, format: .dateTime.weekday(.wide).day())
|
||||||
.font(.system(size: 16, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
if isMissing {
|
if isMissing {
|
||||||
Text(String(localized: "mood_value_missing_tap_to_add"))
|
Text(String(localized: "mood_value_missing_tap_to_add"))
|
||||||
.font(.system(size: 13))
|
.font(.caption)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
} else {
|
} else {
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 13, weight: .medium))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,10 +227,10 @@ struct EntryListView: View {
|
|||||||
// Date column
|
// Date column
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
Text(entry.forDate, format: .dateTime.day())
|
Text(entry.forDate, format: .dateTime.day())
|
||||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
.font(.title3.weight(.bold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.caption2.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
}
|
}
|
||||||
@@ -241,7 +243,7 @@ struct EntryListView: View {
|
|||||||
.frame(height: 32)
|
.frame(height: 32)
|
||||||
.overlay(
|
.overlay(
|
||||||
Text(String(localized: "mood_value_missing_tap_to_add"))
|
Text(String(localized: "mood_value_missing_tap_to_add"))
|
||||||
.font(.system(size: 12, weight: .medium))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -255,9 +257,10 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 16, height: 16)
|
.frame(width: 16, height: 16)
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
|
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 13, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -280,19 +283,20 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 28, height: 28)
|
.frame(width: 28, height: 28)
|
||||||
.foregroundColor(isMissing ? .gray : .white)
|
.foregroundColor(isMissing ? .gray : .white)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.wide).day())
|
Text(entry.forDate, format: .dateTime.weekday(.wide).day())
|
||||||
.font(.system(size: 15, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(isMissing ? textColor : .white)
|
.foregroundColor(isMissing ? textColor : .white)
|
||||||
|
|
||||||
if isMissing {
|
if isMissing {
|
||||||
Text(String(localized: "mood_value_missing_tap_to_add"))
|
Text(String(localized: "mood_value_missing_tap_to_add"))
|
||||||
.font(.system(size: 13))
|
.font(.caption)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
} else {
|
} else {
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 13, weight: .medium))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(.white.opacity(0.85))
|
.foregroundColor(.white.opacity(0.85))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,7 +304,7 @@ struct EntryListView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.font(.caption.weight(.semibold))
|
||||||
.foregroundColor(isMissing ? textColor.opacity(0.3) : .white.opacity(0.6))
|
.foregroundColor(isMissing ? textColor.opacity(0.3) : .white.opacity(0.6))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 18)
|
.padding(.horizontal, 18)
|
||||||
@@ -340,6 +344,7 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.foregroundColor(isMissing ? .gray : .white)
|
.foregroundColor(isMissing ? .gray : .white)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
}
|
}
|
||||||
.shadow(
|
.shadow(
|
||||||
color: isMissing ? .clear : moodColor.opacity(0.3),
|
color: isMissing ? .clear : moodColor.opacity(0.3),
|
||||||
@@ -350,12 +355,12 @@ struct EntryListView: View {
|
|||||||
|
|
||||||
// Day number
|
// Day number
|
||||||
Text(entry.forDate, format: .dateTime.day())
|
Text(entry.forDate, format: .dateTime.day())
|
||||||
.font(.system(size: 16, weight: .bold, design: .rounded))
|
.font(.subheadline.weight(.bold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
// Weekday abbreviation
|
// Weekday abbreviation
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.caption2.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
}
|
}
|
||||||
@@ -372,7 +377,7 @@ struct EntryListView: View {
|
|||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
// Giant day number - the visual hero
|
// Giant day number - the visual hero
|
||||||
Text(entry.forDate, format: .dateTime.day())
|
Text(entry.forDate, format: .dateTime.day())
|
||||||
.font(.system(size: 64, weight: .black, design: .rounded))
|
.font(.largeTitle.weight(.black))
|
||||||
.foregroundStyle(
|
.foregroundStyle(
|
||||||
isMissing
|
isMissing
|
||||||
? LinearGradient(colors: [Color.gray.opacity(0.3), Color.gray.opacity(0.15)], startPoint: .top, endPoint: .bottom)
|
? LinearGradient(colors: [Color.gray.opacity(0.3), Color.gray.opacity(0.15)], startPoint: .top, endPoint: .bottom)
|
||||||
@@ -385,7 +390,7 @@ struct EntryListView: View {
|
|||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
// Weekday with elegant typography
|
// Weekday with elegant typography
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||||
.font(.system(size: 13, weight: .semibold, design: .rounded))
|
.font(.caption.weight(.semibold))
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
.tracking(2)
|
.tracking(2)
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
@@ -423,21 +428,22 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
.foregroundColor(isMissing ? .gray : .white)
|
.foregroundColor(isMissing ? .gray : .white)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
if isMissing {
|
if isMissing {
|
||||||
Text(String(localized: "mood_value_missing_tap_to_add"))
|
Text(String(localized: "mood_value_missing_tap_to_add"))
|
||||||
.font(.system(size: 15, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
} else {
|
} else {
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
.font(.title3.weight(.bold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
// Month context
|
// Month context
|
||||||
Text(entry.forDate, format: .dateTime.month(.wide))
|
Text(entry.forDate, format: .dateTime.month(.wide))
|
||||||
.font(.system(size: 12, weight: .medium))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,12 +516,12 @@ struct EntryListView: View {
|
|||||||
// Left column: Giant day number in serif
|
// Left column: Giant day number in serif
|
||||||
VStack(alignment: .trailing, spacing: 0) {
|
VStack(alignment: .trailing, spacing: 0) {
|
||||||
Text(entry.forDate, format: .dateTime.day())
|
Text(entry.forDate, format: .dateTime.day())
|
||||||
.font(.system(size: 72, weight: .regular, design: .serif))
|
.font(.largeTitle.weight(.regular))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
.frame(width: 80)
|
.frame(width: 80)
|
||||||
|
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated).month(.abbreviated))
|
Text(entry.forDate, format: .dateTime.weekday(.abbreviated).month(.abbreviated))
|
||||||
.font(.system(size: 11, weight: .regular, design: .serif))
|
.font(.caption2.weight(.regular))
|
||||||
.italic()
|
.italic()
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
@@ -530,12 +536,12 @@ struct EntryListView: View {
|
|||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
if isMissing {
|
if isMissing {
|
||||||
Text("Entry Missing")
|
Text("Entry Missing")
|
||||||
.font(.system(size: 24, weight: .regular, design: .serif))
|
.font(.title2.weight(.regular))
|
||||||
.italic()
|
.italic()
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
|
|
||||||
Text("Tap to record your mood for this day")
|
Text("Tap to record your mood for this day")
|
||||||
.font(.system(size: 13, weight: .regular, design: .serif))
|
.font(.caption.weight(.regular))
|
||||||
.foregroundColor(.gray.opacity(0.7))
|
.foregroundColor(.gray.opacity(0.7))
|
||||||
} else {
|
} else {
|
||||||
// Pull-quote style mood name
|
// Pull-quote style mood name
|
||||||
@@ -545,7 +551,7 @@ struct EntryListView: View {
|
|||||||
.frame(width: 4)
|
.frame(width: 4)
|
||||||
|
|
||||||
Text("\"\(entry.moodString)\"")
|
Text("\"\(entry.moodString)\"")
|
||||||
.font(.system(size: 28, weight: .regular, design: .serif))
|
.font(.title.weight(.regular))
|
||||||
.italic()
|
.italic()
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
}
|
}
|
||||||
@@ -557,9 +563,10 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
|
|
||||||
Text("Recorded mood entry")
|
Text("Recorded mood entry")
|
||||||
.font(.system(size: 12, weight: .regular, design: .serif))
|
.font(.caption.weight(.regular))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
.tracking(1.5)
|
.tracking(1.5)
|
||||||
@@ -618,23 +625,24 @@ struct EntryListView: View {
|
|||||||
.frame(width: 28, height: 28)
|
.frame(width: 28, height: 28)
|
||||||
.foregroundColor(isMissing ? .gray : moodColor)
|
.foregroundColor(isMissing ? .gray : moodColor)
|
||||||
.shadow(color: isMissing ? .clear : moodColor, radius: 8, x: 0, y: 0)
|
.shadow(color: isMissing ? .clear : moodColor, radius: 8, x: 0, y: 0)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
}
|
}
|
||||||
.frame(width: 52, height: 52)
|
.frame(width: 52, height: 52)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
// Date in monospace terminal style
|
// Date in monospace terminal style
|
||||||
Text(entry.forDate, format: .dateTime.year().month(.twoDigits).day(.twoDigits))
|
Text(entry.forDate, format: .dateTime.year().month(.twoDigits).day(.twoDigits))
|
||||||
.font(.system(size: 13, weight: .medium, design: .monospaced))
|
.font(.caption.weight(.medium).monospaced())
|
||||||
.foregroundColor(Color(red: 0.4, green: 1.0, blue: 0.4)) // Terminal green
|
.foregroundColor(Color(red: 0.4, green: 1.0, blue: 0.4)) // Terminal green
|
||||||
|
|
||||||
if isMissing {
|
if isMissing {
|
||||||
Text("NO_DATA")
|
Text("NO_DATA")
|
||||||
.font(.system(size: 18, weight: .bold, design: .monospaced))
|
.font(.headline.weight(.bold).monospaced())
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
} else {
|
} else {
|
||||||
// Mood in glowing text
|
// Mood in glowing text
|
||||||
Text(entry.moodString.uppercased())
|
Text(entry.moodString.uppercased())
|
||||||
.font(.system(size: 18, weight: .black, design: .default))
|
.font(.headline.weight(.black))
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
.shadow(color: moodColor.opacity(0.8), radius: 6, x: 0, y: 0)
|
.shadow(color: moodColor.opacity(0.8), radius: 6, x: 0, y: 0)
|
||||||
.shadow(color: moodColor.opacity(0.4), radius: 12, x: 0, y: 0)
|
.shadow(color: moodColor.opacity(0.4), radius: 12, x: 0, y: 0)
|
||||||
@@ -642,7 +650,7 @@ struct EntryListView: View {
|
|||||||
|
|
||||||
// Weekday
|
// Weekday
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||||
.font(.system(size: 11, weight: .medium, design: .monospaced))
|
.font(.caption2.weight(.medium).monospaced())
|
||||||
.foregroundColor(.white.opacity(0.4))
|
.foregroundColor(.white.opacity(0.4))
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
}
|
}
|
||||||
@@ -651,7 +659,7 @@ struct EntryListView: View {
|
|||||||
|
|
||||||
// Chevron with glow
|
// Chevron with glow
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
.font(.system(size: 14, weight: .bold))
|
.font(.subheadline.weight(.bold))
|
||||||
.foregroundColor(isMissing ? .gray : moodColor)
|
.foregroundColor(isMissing ? .gray : moodColor)
|
||||||
.shadow(color: isMissing ? .clear : moodColor, radius: 4, x: 0, y: 0)
|
.shadow(color: isMissing ? .clear : moodColor, radius: 4, x: 0, y: 0)
|
||||||
}
|
}
|
||||||
@@ -713,36 +721,37 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 18, height: 18)
|
.frame(width: 18, height: 18)
|
||||||
.foregroundColor(isMissing ? .gray.opacity(0.5) : moodColor.opacity(0.8))
|
.foregroundColor(isMissing ? .gray.opacity(0.5) : moodColor.opacity(0.8))
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
// Day number with brush-like weight variation
|
// Day number with brush-like weight variation
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||||
Text(entry.forDate, format: .dateTime.day())
|
Text(entry.forDate, format: .dateTime.day())
|
||||||
.font(.system(size: 36, weight: .thin))
|
.font(.title.weight(.thin))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(entry.forDate, format: .dateTime.month(.wide))
|
Text(entry.forDate, format: .dateTime.month(.wide))
|
||||||
.font(.system(size: 11, weight: .light))
|
.font(.caption2.weight(.light))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
.tracking(2)
|
.tracking(2)
|
||||||
|
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||||
.font(.system(size: 11, weight: .light))
|
.font(.caption2.weight(.light))
|
||||||
.foregroundColor(textColor.opacity(0.35))
|
.foregroundColor(textColor.opacity(0.35))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isMissing {
|
if isMissing {
|
||||||
Text("—")
|
Text("—")
|
||||||
.font(.system(size: 20, weight: .ultraLight))
|
.font(.title3.weight(.ultraLight))
|
||||||
.foregroundColor(.gray.opacity(0.4))
|
.foregroundColor(.gray.opacity(0.4))
|
||||||
} else {
|
} else {
|
||||||
// Mood in delicate typography
|
// Mood in delicate typography
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 17, weight: .light))
|
.font(.body.weight(.light))
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
.tracking(1)
|
.tracking(1)
|
||||||
}
|
}
|
||||||
@@ -862,16 +871,17 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 26, height: 26)
|
.frame(width: 26, height: 26)
|
||||||
.foregroundColor(isMissing ? .gray : .white)
|
.foregroundColor(isMissing ? .gray : .white)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||||
.font(.system(size: 15, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
||||||
.font(.system(size: 13, weight: .medium))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.6))
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
|
|
||||||
if !isMissing {
|
if !isMissing {
|
||||||
@@ -880,11 +890,11 @@ struct EntryListView: View {
|
|||||||
.frame(width: 4, height: 4)
|
.frame(width: 4, height: 4)
|
||||||
|
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 13, weight: .semibold))
|
.font(.caption.weight(.semibold))
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
} else {
|
} else {
|
||||||
Text("Tap to add")
|
Text("Tap to add")
|
||||||
.font(.system(size: 13))
|
.font(.caption)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -894,7 +904,7 @@ struct EntryListView: View {
|
|||||||
|
|
||||||
// Prismatic chevron
|
// Prismatic chevron
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundStyle(
|
.foregroundStyle(
|
||||||
isMissing
|
isMissing
|
||||||
? AnyShapeStyle(Color.gray.opacity(0.3))
|
? AnyShapeStyle(Color.gray.opacity(0.3))
|
||||||
@@ -919,7 +929,7 @@ struct EntryListView: View {
|
|||||||
// Track number column
|
// Track number column
|
||||||
VStack {
|
VStack {
|
||||||
Text(entry.forDate, format: .dateTime.day())
|
Text(entry.forDate, format: .dateTime.day())
|
||||||
.font(.system(size: 24, weight: .bold, design: .monospaced))
|
.font(.title2.weight(.bold).monospaced())
|
||||||
.foregroundColor(isMissing ? .gray : moodColor)
|
.foregroundColor(isMissing ? .gray : moodColor)
|
||||||
}
|
}
|
||||||
.frame(width: 50)
|
.frame(width: 50)
|
||||||
@@ -949,17 +959,17 @@ struct EntryListView: View {
|
|||||||
// Track info
|
// Track info
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.wide).month(.abbreviated))
|
Text(entry.forDate, format: .dateTime.weekday(.wide).month(.abbreviated))
|
||||||
.font(.system(size: 11, weight: .medium, design: .monospaced))
|
.font(.caption2.weight(.medium).monospaced())
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
|
|
||||||
if isMissing {
|
if isMissing {
|
||||||
Text("SIDE B - NO RECORDING")
|
Text("SIDE B - NO RECORDING")
|
||||||
.font(.system(size: 14, weight: .bold, design: .rounded))
|
.font(.subheadline.weight(.bold))
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
} else {
|
} else {
|
||||||
Text(entry.moodString.uppercased())
|
Text(entry.moodString.uppercased())
|
||||||
.font(.system(size: 16, weight: .black, design: .rounded))
|
.font(.subheadline.weight(.black))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
.tracking(1)
|
.tracking(1)
|
||||||
}
|
}
|
||||||
@@ -992,6 +1002,7 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 16, height: 16)
|
.frame(width: 16, height: 16)
|
||||||
.foregroundColor(isMissing ? .gray : moodColor)
|
.foregroundColor(isMissing ? .gray : moodColor)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
@@ -1068,21 +1079,22 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 24, height: 24)
|
.frame(width: 24, height: 24)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
// Date with organic flow
|
// Date with organic flow
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Text(entry.forDate, format: .dateTime.day())
|
Text(entry.forDate, format: .dateTime.day())
|
||||||
.font(.system(size: 32, weight: .light))
|
.font(.title.weight(.light))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
Text(entry.forDate, format: .dateTime.month(.abbreviated))
|
Text(entry.forDate, format: .dateTime.month(.abbreviated))
|
||||||
.font(.system(size: 12, weight: .medium))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.6))
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||||
.font(.system(size: 11, weight: .regular))
|
.font(.caption2.weight(.regular))
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
}
|
}
|
||||||
.padding(.leading, 6)
|
.padding(.leading, 6)
|
||||||
@@ -1090,11 +1102,11 @@ struct EntryListView: View {
|
|||||||
|
|
||||||
if isMissing {
|
if isMissing {
|
||||||
Text("No mood recorded")
|
Text("No mood recorded")
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
} else {
|
} else {
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 18, weight: .semibold))
|
.font(.headline.weight(.semibold))
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1139,11 +1151,11 @@ struct EntryListView: View {
|
|||||||
// Handwritten-style date
|
// Handwritten-style date
|
||||||
VStack(alignment: .center, spacing: 2) {
|
VStack(alignment: .center, spacing: 2) {
|
||||||
Text(entry.forDate, format: .dateTime.day())
|
Text(entry.forDate, format: .dateTime.day())
|
||||||
.font(.system(size: 36, weight: .light, design: .serif))
|
.font(.title.weight(.light))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||||
.font(.system(size: 12, weight: .medium, design: .serif))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
}
|
}
|
||||||
@@ -1158,12 +1170,12 @@ struct EntryListView: View {
|
|||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
// Lined paper effect
|
// Lined paper effect
|
||||||
Text(entry.forDate, format: .dateTime.month(.wide).year())
|
Text(entry.forDate, format: .dateTime.month(.wide).year())
|
||||||
.font(.system(size: 12, weight: .regular, design: .serif))
|
.font(.caption.weight(.regular))
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
|
|
||||||
if isMissing {
|
if isMissing {
|
||||||
Text("nothing written...")
|
Text("nothing written...")
|
||||||
.font(.system(size: 18, weight: .regular, design: .serif))
|
.font(.headline.weight(.regular))
|
||||||
.italic()
|
.italic()
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
} else {
|
} else {
|
||||||
@@ -1173,9 +1185,10 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 24, height: 24)
|
.frame(width: 24, height: 24)
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
|
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 20, weight: .medium, design: .serif))
|
.font(.title3.weight(.medium))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1209,11 +1222,11 @@ struct EntryListView: View {
|
|||||||
// Date column - minimal
|
// Date column - minimal
|
||||||
VStack(alignment: .trailing, spacing: 2) {
|
VStack(alignment: .trailing, spacing: 2) {
|
||||||
Text(entry.forDate, format: .dateTime.day())
|
Text(entry.forDate, format: .dateTime.day())
|
||||||
.font(.system(size: 28, weight: .thin))
|
.font(.title.weight(.thin))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||||
.font(.system(size: 10, weight: .medium))
|
.font(.caption2.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
}
|
}
|
||||||
@@ -1263,15 +1276,16 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 28, height: 28)
|
.frame(width: 28, height: 28)
|
||||||
.foregroundColor(isMissing ? .gray : .white)
|
.foregroundColor(isMissing ? .gray : .white)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
.padding(.leading, 16)
|
.padding(.leading, 16)
|
||||||
|
|
||||||
if isMissing {
|
if isMissing {
|
||||||
Text("No entry")
|
Text("No entry")
|
||||||
.font(.system(size: 15, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
} else {
|
} else {
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 17, weight: .semibold))
|
.font(.body.weight(.semibold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1279,7 +1293,7 @@ struct EntryListView: View {
|
|||||||
|
|
||||||
// Month indicator
|
// Month indicator
|
||||||
Text(entry.forDate, format: .dateTime.month(.abbreviated))
|
Text(entry.forDate, format: .dateTime.month(.abbreviated))
|
||||||
.font(.system(size: 11, weight: .bold))
|
.font(.caption2.weight(.bold))
|
||||||
.foregroundColor(isMissing ? .gray : .white.opacity(0.7))
|
.foregroundColor(isMissing ? .gray : .white.opacity(0.7))
|
||||||
.padding(.trailing, 16)
|
.padding(.trailing, 16)
|
||||||
}
|
}
|
||||||
@@ -1309,20 +1323,21 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 28, height: 28)
|
.frame(width: 28, height: 28)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.wide).day())
|
Text(entry.forDate, format: .dateTime.weekday(.wide).day())
|
||||||
.font(.system(size: 17, weight: .semibold))
|
.font(.body.weight(.semibold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
if isMissing {
|
if isMissing {
|
||||||
Text("No mood recorded")
|
Text("No mood recorded")
|
||||||
.font(.system(size: 14))
|
.font(.subheadline)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
} else {
|
} else {
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 15, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1333,7 +1348,7 @@ struct EntryListView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text(entry.forDate, format: .dateTime.month(.abbreviated))
|
Text(entry.forDate, format: .dateTime.month(.abbreviated))
|
||||||
.font(.system(size: 12, weight: .medium))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.6))
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
@@ -1362,6 +1377,7 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: iconSize, height: iconSize)
|
.frame(width: iconSize, height: iconSize)
|
||||||
.foregroundColor(isMissing ? Color.gray.opacity(0.15) : moodColor.opacity(0.2))
|
.foregroundColor(isMissing ? Color.gray.opacity(0.15) : moodColor.opacity(0.2))
|
||||||
|
.accessibilityHidden(true)
|
||||||
.position(
|
.position(
|
||||||
x: CGFloat(col) * spacing + (row.isMultiple(of: 2) ? spacing/2 : 0),
|
x: CGFloat(col) * spacing + (row.isMultiple(of: 2) ? spacing/2 : 0),
|
||||||
y: CGFloat(row) * spacing
|
y: CGFloat(row) * spacing
|
||||||
@@ -1467,21 +1483,22 @@ struct EntryListView: View {
|
|||||||
.frame(width: 22, height: 22)
|
.frame(width: 22, height: 22)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.shadow(color: Color.black.opacity(0.3), radius: 1, x: 0, y: 1)
|
.shadow(color: Color.black.opacity(0.3), radius: 1, x: 0, y: 1)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||||
.font(.system(size: 16, weight: .semibold, design: .serif))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(Color(red: 0.95, green: 0.90, blue: 0.80))
|
.foregroundColor(Color(red: 0.95, green: 0.90, blue: 0.80))
|
||||||
.shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1)
|
.shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1)
|
||||||
|
|
||||||
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
||||||
.font(.system(size: 12, weight: .medium, design: .serif))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(Color(red: 0.8, green: 0.7, blue: 0.55))
|
.foregroundColor(Color(red: 0.8, green: 0.7, blue: 0.55))
|
||||||
|
|
||||||
if !isMissing {
|
if !isMissing {
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 14, weight: .bold, design: .serif))
|
.font(.subheadline.weight(.bold))
|
||||||
.foregroundColor(Color(red: 0.95, green: 0.90, blue: 0.80))
|
.foregroundColor(Color(red: 0.95, green: 0.90, blue: 0.80))
|
||||||
.shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1)
|
.shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1)
|
||||||
}
|
}
|
||||||
@@ -1598,22 +1615,23 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 24, height: 24)
|
.frame(width: 24, height: 24)
|
||||||
.foregroundColor(isMissing ? .gray : .white)
|
.foregroundColor(isMissing ? .gray : .white)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||||
.font(.system(size: 17, weight: .semibold))
|
.font(.body.weight(.semibold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
||||||
.font(.system(size: 13))
|
.font(.caption)
|
||||||
.foregroundColor(textColor.opacity(0.6))
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
|
|
||||||
if !isMissing {
|
if !isMissing {
|
||||||
// Glass pill for mood
|
// Glass pill for mood
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 12, weight: .medium))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
@@ -1633,7 +1651,7 @@ struct EntryListView: View {
|
|||||||
|
|
||||||
// Glass chevron
|
// Glass chevron
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
}
|
}
|
||||||
.padding(18)
|
.padding(18)
|
||||||
@@ -1699,15 +1717,16 @@ struct EntryListView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 18, height: 18)
|
.frame(width: 18, height: 18)
|
||||||
.foregroundColor(isMissing ? .gray : moodColor)
|
.foregroundColor(isMissing ? .gray : moodColor)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
|
|
||||||
// Date - very compact
|
// Date - very compact
|
||||||
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
||||||
.font(.system(size: 13, weight: .medium, design: .monospaced))
|
.font(.caption.weight(.medium).monospaced())
|
||||||
.foregroundColor(textColor.opacity(0.7))
|
.foregroundColor(textColor.opacity(0.7))
|
||||||
|
|
||||||
// Weekday initial
|
// Weekday initial
|
||||||
Text(String(Random.weekdayName(fromDate: entry.forDate).prefix(3)))
|
Text(String(Random.weekdayName(fromDate: entry.forDate).prefix(3)))
|
||||||
.font(.system(size: 11, weight: .semibold))
|
.font(.caption2.weight(.semibold))
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
.frame(width: 28)
|
.frame(width: 28)
|
||||||
|
|
||||||
@@ -1716,7 +1735,7 @@ struct EntryListView: View {
|
|||||||
// Mood as tiny pill
|
// Mood as tiny pill
|
||||||
if !isMissing {
|
if !isMissing {
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 11, weight: .semibold))
|
.font(.caption2.weight(.semibold))
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 3)
|
.padding(.vertical, 3)
|
||||||
@@ -1726,12 +1745,12 @@ struct EntryListView: View {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text("tap")
|
Text("tap")
|
||||||
.font(.system(size: 10, weight: .medium))
|
.font(.caption2.weight(.medium))
|
||||||
.foregroundColor(.gray.opacity(0.6))
|
.foregroundColor(.gray.opacity(0.6))
|
||||||
}
|
}
|
||||||
|
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
.font(.system(size: 10, weight: .semibold))
|
.font(.caption2.weight(.semibold))
|
||||||
.foregroundColor(textColor.opacity(0.25))
|
.foregroundColor(textColor.opacity(0.25))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 14)
|
.padding(.horizontal, 14)
|
||||||
@@ -1855,12 +1874,13 @@ struct MotionCardView: View {
|
|||||||
x: -motionManager.xOffset * 0.3,
|
x: -motionManager.xOffset * 0.3,
|
||||||
y: -motionManager.yOffset * 0.3
|
y: -motionManager.yOffset * 0.3
|
||||||
)
|
)
|
||||||
|
.accessibilityLabel(entry.mood.strValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
// Day with motion
|
// Day with motion
|
||||||
Text("\(dayNumber)")
|
Text("\(dayNumber)")
|
||||||
.font(.system(size: 32, weight: .bold, design: .rounded))
|
.font(.title.weight(.bold))
|
||||||
.foregroundColor(isMissing ? .gray : moodColor)
|
.foregroundColor(isMissing ? .gray : moodColor)
|
||||||
.offset(
|
.offset(
|
||||||
x: motionManager.xOffset * 0.2,
|
x: motionManager.xOffset * 0.2,
|
||||||
@@ -1869,7 +1889,7 @@ struct MotionCardView: View {
|
|||||||
|
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text(Random.weekdayName(fromDate: entry.forDate))
|
Text(Random.weekdayName(fromDate: entry.forDate))
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.7))
|
.foregroundColor(textColor.opacity(0.7))
|
||||||
|
|
||||||
if !isMissing {
|
if !isMissing {
|
||||||
@@ -1877,7 +1897,7 @@ struct MotionCardView: View {
|
|||||||
.fill(moodColor)
|
.fill(moodColor)
|
||||||
.frame(width: 4, height: 4)
|
.frame(width: 4, height: 4)
|
||||||
Text(entry.moodString)
|
Text(entry.moodString)
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(moodColor)
|
.foregroundColor(moodColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1886,7 +1906,7 @@ struct MotionCardView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(textColor.opacity(0.3))
|
.foregroundColor(textColor.opacity(0.3))
|
||||||
.offset(
|
.offset(
|
||||||
x: motionManager.xOffset * 0.5,
|
x: motionManager.xOffset * 0.5,
|
||||||
@@ -1924,7 +1944,9 @@ class MotionManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func startMotionUpdates() {
|
private func startMotionUpdates() {
|
||||||
guard motionManager.isDeviceMotionAvailable else { return }
|
// Respect Reduce Motion preference - skip parallax effect entirely
|
||||||
|
guard motionManager.isDeviceMotionAvailable,
|
||||||
|
!UIAccessibility.isReduceMotionEnabled else { return }
|
||||||
|
|
||||||
motionManager.deviceMotionUpdateInterval = 1/60
|
motionManager.deviceMotionUpdateInterval = 1/60
|
||||||
motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motion, error in
|
motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motion, error in
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ struct FeelsSubscriptionStoreView: View {
|
|||||||
.frame(width: 100, height: 100)
|
.frame(width: 100, height: 100)
|
||||||
|
|
||||||
Image(systemName: "heart.fill")
|
Image(systemName: "heart.fill")
|
||||||
.font(.system(size: 44))
|
.font(.largeTitle)
|
||||||
.foregroundStyle(
|
.foregroundStyle(
|
||||||
LinearGradient(
|
LinearGradient(
|
||||||
colors: [.pink, .red],
|
colors: [.pink, .red],
|
||||||
@@ -40,10 +40,10 @@ struct FeelsSubscriptionStoreView: View {
|
|||||||
|
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
Text("Unlock Premium")
|
Text("Unlock Premium")
|
||||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
.font(.title.weight(.bold))
|
||||||
|
|
||||||
Text("Get unlimited access to all features")
|
Text("Get unlimited access to all features")
|
||||||
.font(.system(size: 16))
|
.font(.body)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
}
|
}
|
||||||
@@ -80,11 +80,11 @@ struct FeatureHighlight: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.font(.system(size: 18))
|
.font(.headline)
|
||||||
.foregroundColor(.green)
|
.foregroundColor(.green)
|
||||||
|
|
||||||
Text(text)
|
Text(text)
|
||||||
.font(.system(size: 15, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ struct InsightsView: View {
|
|||||||
// Header
|
// Header
|
||||||
HStack {
|
HStack {
|
||||||
Text("Insights")
|
Text("Insights")
|
||||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
.font(.title.weight(.bold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ struct InsightsView: View {
|
|||||||
if viewModel.isAIAvailable {
|
if viewModel.isAIAvailable {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: "sparkles")
|
Image(systemName: "sparkles")
|
||||||
.font(.system(size: 12, weight: .medium))
|
.font(.caption.weight(.medium))
|
||||||
Text("AI")
|
Text("AI")
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.font(.caption.weight(.semibold))
|
||||||
}
|
}
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
@@ -118,7 +118,7 @@ struct InsightsView: View {
|
|||||||
.frame(width: 100, height: 100)
|
.frame(width: 100, height: 100)
|
||||||
|
|
||||||
Image(systemName: "sparkles")
|
Image(systemName: "sparkles")
|
||||||
.font(.system(size: 44))
|
.font(.largeTitle)
|
||||||
.foregroundStyle(
|
.foregroundStyle(
|
||||||
LinearGradient(
|
LinearGradient(
|
||||||
colors: [.purple, .blue],
|
colors: [.purple, .blue],
|
||||||
@@ -131,12 +131,12 @@ struct InsightsView: View {
|
|||||||
// Text
|
// Text
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
Text("Unlock AI-Powered Insights")
|
Text("Unlock AI-Powered Insights")
|
||||||
.font(.system(size: 24, weight: .bold, design: .rounded))
|
.font(.title2.weight(.bold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
Text("Discover patterns in your mood, get personalized recommendations, and understand what affects how you feel.")
|
Text("Discover patterns in your mood, get personalized recommendations, and understand what affects how you feel.")
|
||||||
.font(.system(size: 16))
|
.font(.body)
|
||||||
.foregroundColor(textColor.opacity(0.7))
|
.foregroundColor(textColor.opacity(0.7))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, 32)
|
.padding(.horizontal, 32)
|
||||||
@@ -150,7 +150,7 @@ struct InsightsView: View {
|
|||||||
Image(systemName: "sparkles")
|
Image(systemName: "sparkles")
|
||||||
Text("Get Personal Insights")
|
Text("Get Personal Insights")
|
||||||
}
|
}
|
||||||
.font(.system(size: 18, weight: .bold))
|
.font(.headline.weight(.bold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, 16)
|
.padding(.vertical, 16)
|
||||||
@@ -202,14 +202,20 @@ struct InsightsSectionView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Section Header
|
// Section Header
|
||||||
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() } }) {
|
Button(action: {
|
||||||
|
if UIAccessibility.isReduceMotionEnabled {
|
||||||
|
isExpanded.toggle()
|
||||||
|
} else {
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() }
|
||||||
|
}
|
||||||
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.system(size: 18, weight: .medium))
|
.font(.headline.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.6))
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
|
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: 20, weight: .bold))
|
.font(.title3.weight(.bold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
// Loading indicator in header
|
// Loading indicator in header
|
||||||
@@ -222,7 +228,7 @@ struct InsightsSectionView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.font(.caption.weight(.semibold))
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
@@ -277,6 +283,7 @@ struct InsightsSectionView: View {
|
|||||||
removal: .opacity
|
removal: .opacity
|
||||||
))
|
))
|
||||||
.animation(
|
.animation(
|
||||||
|
UIAccessibility.isReduceMotionEnabled ? nil :
|
||||||
.spring(response: 0.4, dampingFraction: 0.8)
|
.spring(response: 0.4, dampingFraction: 0.8)
|
||||||
.delay(Double(index) * 0.05),
|
.delay(Double(index) * 0.05),
|
||||||
value: insights.count
|
value: insights.count
|
||||||
@@ -294,7 +301,7 @@ struct InsightsSectionView: View {
|
|||||||
.fill(colorScheme == .dark ? Color(.systemGray6) : .white)
|
.fill(colorScheme == .dark ? Color(.systemGray6) : .white)
|
||||||
)
|
)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.animation(.easeInOut(duration: 0.2), value: isExpanded)
|
.animation(UIAccessibility.isReduceMotionEnabled ? nil : .easeInOut(duration: 0.2), value: isExpanded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,15 +343,18 @@ struct InsightSkeletonView: View {
|
|||||||
)
|
)
|
||||||
.opacity(isAnimating ? 0.6 : 1.0)
|
.opacity(isAnimating ? 0.6 : 1.0)
|
||||||
.animation(
|
.animation(
|
||||||
|
UIAccessibility.isReduceMotionEnabled ? nil :
|
||||||
.easeInOut(duration: 0.8)
|
.easeInOut(duration: 0.8)
|
||||||
.repeatForever(autoreverses: true),
|
.repeatForever(autoreverses: true),
|
||||||
value: isAnimating
|
value: isAnimating
|
||||||
)
|
)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
if !UIAccessibility.isReduceMotionEnabled {
|
||||||
isAnimating = true
|
isAnimating = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Insight Card View
|
// MARK: - Insight Card View
|
||||||
|
|
||||||
@@ -376,9 +386,10 @@ struct InsightCardView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 22, height: 22)
|
.frame(width: 22, height: 22)
|
||||||
.foregroundColor(accentColor)
|
.foregroundColor(accentColor)
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: insight.icon)
|
Image(systemName: insight.icon)
|
||||||
.font(.system(size: 18, weight: .semibold))
|
.font(.headline.weight(.semibold))
|
||||||
.foregroundColor(accentColor)
|
.foregroundColor(accentColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -386,11 +397,11 @@ struct InsightCardView: View {
|
|||||||
// Text Content
|
// Text Content
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(insight.title)
|
Text(insight.title)
|
||||||
.font(.system(size: 15, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Text(insight.description)
|
Text(insight.description)
|
||||||
.font(.system(size: 14))
|
.font(.subheadline)
|
||||||
.foregroundColor(textColor.opacity(0.7))
|
.foregroundColor(textColor.opacity(0.7))
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct LockScreenView: View {
|
|||||||
// App icon / lock icon
|
// App icon / lock icon
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
Image(systemName: "lock.fill")
|
Image(systemName: "lock.fill")
|
||||||
.font(.system(size: 60))
|
.font(.largeTitle)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
Text("Feels is Locked")
|
Text("Feels is Locked")
|
||||||
|
|||||||
@@ -216,13 +216,13 @@ struct MonthCard: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Header with month/year
|
// Header with month/year
|
||||||
Text("\(Random.monthName(fromMonthInt: month).uppercased()) \(String(year))")
|
Text("\(Random.monthName(fromMonthInt: month).uppercased()) \(String(year))")
|
||||||
.font(.system(size: 32, weight: .heavy, design: .rounded))
|
.font(.title.weight(.heavy))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
.padding(.top, 40)
|
.padding(.top, 40)
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
|
|
||||||
Text("Monthly Mood Wrap")
|
Text("Monthly Mood Wrap")
|
||||||
.font(.system(size: 16, weight: .medium, design: .rounded))
|
.font(.body.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.6))
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
.padding(.bottom, 30)
|
.padding(.bottom, 30)
|
||||||
|
|
||||||
@@ -238,15 +238,16 @@ struct MonthCard: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(24)
|
.padding(24)
|
||||||
|
.accessibilityLabel(topMood.strValue)
|
||||||
)
|
)
|
||||||
.shadow(color: moodTint.color(forMood: topMood).opacity(0.5), radius: 20, x: 0, y: 10)
|
.shadow(color: moodTint.color(forMood: topMood).opacity(0.5), radius: 20, x: 0, y: 10)
|
||||||
|
|
||||||
Text("Top Mood")
|
Text("Top Mood")
|
||||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
|
|
||||||
Text(topMood.strValue.uppercased())
|
Text(topMood.strValue.uppercased())
|
||||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
.font(.title3.weight(.bold))
|
||||||
.foregroundColor(moodTint.color(forMood: topMood))
|
.foregroundColor(moodTint.color(forMood: topMood))
|
||||||
}
|
}
|
||||||
.padding(.bottom, 30)
|
.padding(.bottom, 30)
|
||||||
@@ -256,10 +257,10 @@ struct MonthCard: View {
|
|||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
Text("\(totalTrackedDays)")
|
Text("\(totalTrackedDays)")
|
||||||
.font(.system(size: 36, weight: .bold, design: .rounded))
|
.font(.largeTitle.weight(.bold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
Text("Days Tracked")
|
Text("Days Tracked")
|
||||||
.font(.system(size: 12, weight: .medium, design: .rounded))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
@@ -279,6 +280,7 @@ struct MonthCard: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(7)
|
.padding(7)
|
||||||
|
.accessibilityLabel(metric.mood.strValue)
|
||||||
)
|
)
|
||||||
|
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
@@ -294,7 +296,7 @@ struct MonthCard: View {
|
|||||||
.frame(height: 12)
|
.frame(height: 12)
|
||||||
|
|
||||||
Text("\(Int(metric.percent))%")
|
Text("\(Int(metric.percent))%")
|
||||||
.font(.system(size: 14, weight: .semibold, design: .rounded))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
.frame(width: 40, alignment: .trailing)
|
.frame(width: 40, alignment: .trailing)
|
||||||
}
|
}
|
||||||
@@ -305,7 +307,7 @@ struct MonthCard: View {
|
|||||||
|
|
||||||
// App branding
|
// App branding
|
||||||
Text("ifeel")
|
Text("ifeel")
|
||||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.3))
|
.foregroundColor(textColor.opacity(0.3))
|
||||||
.padding(.bottom, 20)
|
.padding(.bottom, 20)
|
||||||
}
|
}
|
||||||
@@ -317,7 +319,13 @@ struct MonthCard: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Month Header
|
// Month Header
|
||||||
HStack {
|
HStack {
|
||||||
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() } }) {
|
Button(action: {
|
||||||
|
if UIAccessibility.isReduceMotionEnabled {
|
||||||
|
showStats.toggle()
|
||||||
|
} else {
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() }
|
||||||
|
}
|
||||||
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
||||||
.font(.title3.bold())
|
.font(.title3.bold())
|
||||||
@@ -453,6 +461,7 @@ struct MoodBarChart: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 18, height: 18)
|
.frame(width: 18, height: 18)
|
||||||
.foregroundColor(moodTint.color(forMood: metric.mood))
|
.foregroundColor(moodTint.color(forMood: metric.mood))
|
||||||
|
.accessibilityLabel(metric.mood.strValue)
|
||||||
|
|
||||||
// Bar
|
// Bar
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
@@ -491,7 +500,7 @@ extension MonthView {
|
|||||||
}, label: {
|
}, label: {
|
||||||
Image(systemName: "gear")
|
Image(systemName: "gear")
|
||||||
.foregroundColor(Color(UIColor.darkGray))
|
.foregroundColor(Color(UIColor.darkGray))
|
||||||
.font(.system(size: 20))
|
.font(.title3)
|
||||||
}).sheet(isPresented: $showingSheet) {
|
}).sheet(isPresented: $showingSheet) {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ struct EntryDetailView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 34, height: 34)
|
.frame(width: 34, height: 34)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
.accessibilityLabel(currentMood.strValue)
|
||||||
}
|
}
|
||||||
.shadow(color: moodColor.opacity(0.4), radius: 8, x: 0, y: 4)
|
.shadow(color: moodColor.opacity(0.4), radius: 8, x: 0, y: 4)
|
||||||
|
|
||||||
@@ -304,9 +305,13 @@ struct EntryDetailView: View {
|
|||||||
ForEach(Mood.allValues) { mood in
|
ForEach(Mood.allValues) { mood in
|
||||||
Button {
|
Button {
|
||||||
// Update local state immediately for instant feedback
|
// Update local state immediately for instant feedback
|
||||||
|
if UIAccessibility.isReduceMotionEnabled {
|
||||||
|
selectedMood = mood
|
||||||
|
} else {
|
||||||
withAnimation(.easeInOut(duration: 0.15)) {
|
withAnimation(.easeInOut(duration: 0.15)) {
|
||||||
selectedMood = mood
|
selectedMood = mood
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Then persist the change
|
// Then persist the change
|
||||||
onMoodUpdate(mood)
|
onMoodUpdate(mood)
|
||||||
} label: {
|
} label: {
|
||||||
@@ -320,6 +325,7 @@ struct EntryDetailView: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 28, height: 28)
|
.frame(width: 28, height: 28)
|
||||||
.foregroundColor(currentMood == mood ? .white : .gray)
|
.foregroundColor(currentMood == mood ? .white : .gray)
|
||||||
|
.accessibilityLabel(mood.strValue)
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(mood.strValue)
|
Text(mood.strValue)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ struct PhotoPickerView: View {
|
|||||||
// Header
|
// Header
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
Image(systemName: "photo.on.rectangle.angled")
|
Image(systemName: "photo.on.rectangle.angled")
|
||||||
.font(.system(size: 50))
|
.font(.largeTitle)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
Text("Add a Photo")
|
Text("Add a Photo")
|
||||||
@@ -281,7 +281,7 @@ struct PhotoGalleryView: View {
|
|||||||
} else {
|
} else {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
Image(systemName: "photo.badge.exclamationmark")
|
Image(systemName: "photo.badge.exclamationmark")
|
||||||
.font(.system(size: 50))
|
.font(.largeTitle)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
|
|
||||||
Text("Photo not found")
|
Text("Photo not found")
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ struct SettingsTabView: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Header
|
// Header
|
||||||
Text("Settings")
|
Text("Settings")
|
||||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
.font(.title.weight(.bold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
@@ -92,14 +92,14 @@ struct UpgradeBannerView: View {
|
|||||||
// Countdown timer
|
// Countdown timer
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Image(systemName: "clock")
|
Image(systemName: "clock")
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(.orange)
|
.foregroundColor(.orange)
|
||||||
|
|
||||||
if let expirationDate = trialExpirationDate {
|
if let expirationDate = trialExpirationDate {
|
||||||
Text("\(Text("Trial expires in ").font(.system(size: 14, weight: .medium)).foregroundColor(textColor.opacity(0.8)))\(Text(expirationDate, style: .relative).font(.system(size: 14, weight: .bold)).foregroundColor(.orange))")
|
Text("\(Text("Trial expires in ").font(.subheadline.weight(.medium)).foregroundColor(textColor.opacity(0.8)))\(Text(expirationDate, style: .relative).font(.subheadline.weight(.bold)).foregroundColor(.orange))")
|
||||||
} else {
|
} else {
|
||||||
Text("Trial expired")
|
Text("Trial expired")
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(.orange)
|
.foregroundColor(.orange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ struct UpgradeBannerView: View {
|
|||||||
showWhyUpgrade = true
|
showWhyUpgrade = true
|
||||||
} label: {
|
} label: {
|
||||||
Text("Why Upgrade?")
|
Text("Why Upgrade?")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
@@ -126,7 +126,7 @@ struct UpgradeBannerView: View {
|
|||||||
showSubscriptionStore = true
|
showSubscriptionStore = true
|
||||||
} label: {
|
} label: {
|
||||||
Text("Subscribe")
|
Text("Subscribe")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
@@ -157,7 +157,7 @@ struct WhyUpgradeView: View {
|
|||||||
// Header
|
// Header
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
Image(systemName: "star.fill")
|
Image(systemName: "star.fill")
|
||||||
.font(.system(size: 50))
|
.font(.largeTitle)
|
||||||
.foregroundStyle(
|
.foregroundStyle(
|
||||||
LinearGradient(
|
LinearGradient(
|
||||||
colors: [.orange, .pink],
|
colors: [.orange, .pink],
|
||||||
@@ -167,7 +167,7 @@ struct WhyUpgradeView: View {
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text("Unlock Premium")
|
Text("Unlock Premium")
|
||||||
.font(.system(size: 28, weight: .bold))
|
.font(.title.weight(.bold))
|
||||||
|
|
||||||
Text("Get the most out of your mood tracking journey")
|
Text("Get the most out of your mood tracking journey")
|
||||||
.font(.body)
|
.font(.body)
|
||||||
@@ -262,7 +262,7 @@ struct PremiumBenefitRow: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .top, spacing: 14) {
|
HStack(alignment: .top, spacing: 14) {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.system(size: 22))
|
.font(.title3)
|
||||||
.foregroundColor(iconColor)
|
.foregroundColor(iconColor)
|
||||||
.frame(width: 44, height: 44)
|
.frame(width: 44, height: 44)
|
||||||
.background(
|
.background(
|
||||||
@@ -272,10 +272,10 @@ struct PremiumBenefitRow: View {
|
|||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.font(.body.weight(.semibold))
|
||||||
|
|
||||||
Text(description)
|
Text(description)
|
||||||
.font(.system(size: 14))
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,13 +174,13 @@ struct YearCard: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Header with year
|
// Header with year
|
||||||
Text(String(year))
|
Text(String(year))
|
||||||
.font(.system(size: 48, weight: .heavy, design: .rounded))
|
.font(.largeTitle.weight(.heavy))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
.padding(.top, 40)
|
.padding(.top, 40)
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
|
|
||||||
Text("Year in Review")
|
Text("Year in Review")
|
||||||
.font(.system(size: 18, weight: .medium, design: .rounded))
|
.font(.headline.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.6))
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
.padding(.bottom, 30)
|
.padding(.bottom, 30)
|
||||||
|
|
||||||
@@ -196,15 +196,16 @@ struct YearCard: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(28)
|
.padding(28)
|
||||||
|
.accessibilityLabel(topMood.strValue)
|
||||||
)
|
)
|
||||||
.shadow(color: moodTint.color(forMood: topMood).opacity(0.5), radius: 25, x: 0, y: 12)
|
.shadow(color: moodTint.color(forMood: topMood).opacity(0.5), radius: 25, x: 0, y: 12)
|
||||||
|
|
||||||
Text("Top Mood")
|
Text("Top Mood")
|
||||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
|
|
||||||
Text(topMood.strValue.uppercased())
|
Text(topMood.strValue.uppercased())
|
||||||
.font(.system(size: 24, weight: .bold, design: .rounded))
|
.font(.title2.weight(.bold))
|
||||||
.foregroundColor(moodTint.color(forMood: topMood))
|
.foregroundColor(moodTint.color(forMood: topMood))
|
||||||
}
|
}
|
||||||
.padding(.bottom, 30)
|
.padding(.bottom, 30)
|
||||||
@@ -214,10 +215,10 @@ struct YearCard: View {
|
|||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
Text("\(totalEntries)")
|
Text("\(totalEntries)")
|
||||||
.font(.system(size: 42, weight: .bold, design: .rounded))
|
.font(.largeTitle.weight(.bold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
Text("Days Tracked")
|
Text("Days Tracked")
|
||||||
.font(.system(size: 13, weight: .medium, design: .rounded))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
@@ -237,6 +238,7 @@ struct YearCard: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
|
.accessibilityLabel(metric.mood.strValue)
|
||||||
)
|
)
|
||||||
|
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
@@ -252,7 +254,7 @@ struct YearCard: View {
|
|||||||
.frame(height: 16)
|
.frame(height: 16)
|
||||||
|
|
||||||
Text("\(Int(metric.percent))%")
|
Text("\(Int(metric.percent))%")
|
||||||
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
.font(.body.weight(.semibold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
.frame(width: 45, alignment: .trailing)
|
.frame(width: 45, alignment: .trailing)
|
||||||
}
|
}
|
||||||
@@ -263,7 +265,7 @@ struct YearCard: View {
|
|||||||
|
|
||||||
// App branding
|
// App branding
|
||||||
Text("ifeel")
|
Text("ifeel")
|
||||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.3))
|
.foregroundColor(textColor.opacity(0.3))
|
||||||
.padding(.bottom, 20)
|
.padding(.bottom, 20)
|
||||||
}
|
}
|
||||||
@@ -275,7 +277,13 @@ struct YearCard: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Year Header
|
// Year Header
|
||||||
HStack {
|
HStack {
|
||||||
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() } }) {
|
Button(action: {
|
||||||
|
if UIAccessibility.isReduceMotionEnabled {
|
||||||
|
showStats.toggle()
|
||||||
|
} else {
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() }
|
||||||
|
}
|
||||||
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(String(year))
|
Text(String(year))
|
||||||
.font(.title2.bold())
|
.font(.title2.bold())
|
||||||
@@ -324,6 +332,7 @@ struct YearCard: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 16, height: 16)
|
.frame(width: 16, height: 16)
|
||||||
.foregroundColor(moodTint.color(forMood: metric.mood))
|
.foregroundColor(moodTint.color(forMood: metric.mood))
|
||||||
|
.accessibilityLabel(metric.mood.strValue)
|
||||||
|
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
@@ -357,7 +366,7 @@ struct YearCard: View {
|
|||||||
HStack(spacing: 2) {
|
HStack(spacing: 2) {
|
||||||
ForEach(months.indices, id: \.self) { index in
|
ForEach(months.indices, id: \.self) { index in
|
||||||
Text(months[index])
|
Text(months[index])
|
||||||
.font(.system(size: 9, weight: .medium))
|
.font(.caption2.weight(.medium))
|
||||||
.foregroundColor(textColor.opacity(0.5))
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -554,6 +554,7 @@
|
|||||||
.feature-card:nth-child(4) .feature-icon { background: rgba(239, 190, 154, 0.2); }
|
.feature-card:nth-child(4) .feature-icon { background: rgba(239, 190, 154, 0.2); }
|
||||||
.feature-card:nth-child(5) .feature-icon { background: rgba(165, 196, 212, 0.2); }
|
.feature-card:nth-child(5) .feature-icon { background: rgba(165, 196, 212, 0.2); }
|
||||||
.feature-card:nth-child(6) .feature-icon { background: rgba(229, 168, 154, 0.2); }
|
.feature-card:nth-child(6) .feature-icon { background: rgba(229, 168, 154, 0.2); }
|
||||||
|
.feature-card:nth-child(7) .feature-icon { background: rgba(94, 186, 175, 0.2); }
|
||||||
|
|
||||||
.feature-card h3 {
|
.feature-card h3 {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
@@ -1191,6 +1192,12 @@
|
|||||||
<h3>Private by Design</h3>
|
<h3>Private by Design</h3>
|
||||||
<p>Your feelings are yours alone. All data stays on your devices with iCloud sync you control.</p>
|
<p>Your feelings are yours alone. All data stays on your devices with iCloud sync you control.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card reveal">
|
||||||
|
<div class="feature-icon">♿</div>
|
||||||
|
<h3>WCAG 2.1 AA Accessible</h3>
|
||||||
|
<p>Built for everyone. Full VoiceOver support, Dynamic Type, and high contrast ensure no one is left behind.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1107,6 +1107,12 @@
|
|||||||
<h3>Privacy</h3>
|
<h3>Privacy</h3>
|
||||||
<p>Your data stays on your devices. iCloud sync with no third-party access.</p>
|
<p>Your data stays on your devices. iCloud sync with no third-party access.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card fade-in">
|
||||||
|
<div class="feature-number">07</div>
|
||||||
|
<h3>WCAG 2.1 AA</h3>
|
||||||
|
<p>Built for everyone. Full VoiceOver support, Dynamic Type, and high contrast ensure no one is left behind.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -491,6 +491,7 @@
|
|||||||
.feature-icon.purple { background: rgba(168, 85, 247, 0.15); }
|
.feature-icon.purple { background: rgba(168, 85, 247, 0.15); }
|
||||||
.feature-icon.red { background: rgba(248, 113, 113, 0.15); }
|
.feature-icon.red { background: rgba(248, 113, 113, 0.15); }
|
||||||
.feature-icon.gray { background: rgba(148, 163, 184, 0.15); }
|
.feature-icon.gray { background: rgba(148, 163, 184, 0.15); }
|
||||||
|
.feature-icon.cyan { background: rgba(34, 211, 238, 0.15); }
|
||||||
|
|
||||||
.feature-card h3 {
|
.feature-card h3 {
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
@@ -956,6 +957,11 @@
|
|||||||
<h3>Privacy First</h3>
|
<h3>Privacy First</h3>
|
||||||
<p>Your feelings stay yours. All data lives on your devices with iCloud sync.</p>
|
<p>Your feelings stay yours. All data lives on your devices with iCloud sync.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="feature-card reveal">
|
||||||
|
<div class="feature-icon cyan">♿</div>
|
||||||
|
<h3>WCAG 2.1 AA Accessible</h3>
|
||||||
|
<p>Built for everyone. Full VoiceOver support, Dynamic Type, and high contrast ensure no one is left behind.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user