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",
|
||||
"strings" : {
|
||||
"": {},
|
||||
"" : {
|
||||
|
||||
},
|
||||
" " : {
|
||||
"comment" : "A placeholder text used to create spacing between list items.",
|
||||
"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.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": "%1$@ '%2$@"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%1$@ '%2$@"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "%1$@ '%2$@"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"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.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": "%1$@ %2$@"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%1$@ %2$@"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "%1$@ %2$@"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
@@ -218,18 +220,18 @@
|
||||
"comment" : "A label that combines the mood description with the date it was recorded, separated by a space.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": "%1$@ on %2$@"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%1$@ am %2$@"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "%1$@ on %2$@"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
@@ -266,18 +268,18 @@
|
||||
"comment" : "The current version of the app, displayed in a small, secondary font.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": "%1$@ v %2$@ (Build %3$@)"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%1$@ v %2$@ (Build %3$@)"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "%1$@ v %2$@ (Build %3$@)"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"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.",
|
||||
"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" : {
|
||||
"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,
|
||||
"localizations" : {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": "%1$lld/%2$lld"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%1$lld/%2$lld"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "%1$lld/%2$lld"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
@@ -524,8 +558,12 @@
|
||||
"comment" : "A symbol that appears before a command in a terminal interface.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"12": {},
|
||||
"17": {},
|
||||
"12" : {
|
||||
|
||||
},
|
||||
"17" : {
|
||||
|
||||
},
|
||||
"20" : {
|
||||
"comment" : "A placeholder text that appears in place of a number.",
|
||||
"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!" : {
|
||||
"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,
|
||||
@@ -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" : {
|
||||
"comment" : "The title of the toggle that enables or disables syncing mood data with Apple Health.",
|
||||
"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." : {
|
||||
"comment" : "An alert message displayed when the user attempts to delete a mood entry.",
|
||||
"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" : {
|
||||
"extractionState" : "manual",
|
||||
"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" : {
|
||||
"comment" : "A button label that triggers the editing of a journal note.",
|
||||
"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..." : {
|
||||
"comment" : "A label indicating that a mood data export is in progress.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -6115,6 +6177,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Log this mood" : {
|
||||
|
||||
},
|
||||
"Log your mood daily to build a streak. Consistency helps you understand your patterns." : {
|
||||
"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" : {
|
||||
"comment" : "Title of an app shortcut that shows the user their mood streak.",
|
||||
"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" : {
|
||||
"comment" : "Title of the app intent to open the Feels app.",
|
||||
"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" : {
|
||||
"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,
|
||||
@@ -8871,6 +8964,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Premium feature, subscription required" : {
|
||||
"comment" : "A description of a premium feature that requires a subscription.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Privacy Lock" : {
|
||||
"comment" : "A title for a toggle that controls whether or not biometric authentication is enabled.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -9823,6 +9920,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Reminder time" : {
|
||||
"comment" : "An accessibility label for the time picker in the onboarding flow.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Reminder Time" : {
|
||||
"comment" : "A label displayed above the reminder time in the settings view.",
|
||||
"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" : {
|
||||
"comment" : "A button label that resets the app's launch date to the current date.",
|
||||
"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" : {
|
||||
"comment" : "The title of a screen that lets a user set the start date of a free trial.",
|
||||
"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" : {
|
||||
"comment" : "A label in the expanded view that describes the current streak of days the user has logged in.",
|
||||
"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" : {
|
||||
"comment" : "A hint displayed below the feature rows, instructing users to swipe to continue.",
|
||||
"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." : {
|
||||
"comment" : "A tip that instructs the user to switch between different time views to view their mood history.",
|
||||
"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" : {
|
||||
"comment" : "A tip to sync their data with Apple Health.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -11937,6 +12066,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Syncing health data" : {
|
||||
"comment" : "A label indicating that health data is being synced.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Take Photo" : {
|
||||
"comment" : "A button that takes a photo using the device's camera.",
|
||||
"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" : {
|
||||
"comment" : "A description of an action a user can take to log their mood.",
|
||||
"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" : {
|
||||
"comment" : "A description of what a user can do to add a new entry to their mood journal.",
|
||||
"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" : {
|
||||
"comment" : "A section header that indicates that the settings view contains only test data.",
|
||||
"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" : {
|
||||
"comment" : "A tip title for viewing and managing one's mood history.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
|
||||
@@ -227,31 +227,31 @@ struct VotingView: View {
|
||||
|
||||
// MARK: - Medium Widget: Single row
|
||||
private var mediumLayout: some View {
|
||||
VStack {
|
||||
VStack(spacing: 12) {
|
||||
Text(hasSubscription ? promptText : "Subscribe to track your mood")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.8)
|
||||
.padding(.bottom, 20)
|
||||
|
||||
HStack(spacing: 16) {
|
||||
HStack(spacing: 0) {
|
||||
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
|
||||
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)
|
||||
|
||||
if hasSubscription {
|
||||
// Active subscription: vote normally
|
||||
Button(intent: VoteMoodIntent(mood: mood)) {
|
||||
moodIcon(for: mood, size: size)
|
||||
.frame(minWidth: touchSize, minHeight: touchSize)
|
||||
@@ -260,7 +260,6 @@ struct VotingView: View {
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Log this mood"))
|
||||
} else {
|
||||
// Trial expired: open app to subscribe
|
||||
Link(destination: URL(string: "feels://subscribe")!) {
|
||||
moodIcon(for: mood, size: size)
|
||||
.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 {
|
||||
moodImages.icon(forMood: mood)
|
||||
.resizable()
|
||||
@@ -315,11 +347,13 @@ struct VotedStatsView: View {
|
||||
|
||||
// Checkmark badge
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 18))
|
||||
.font(.headline)
|
||||
.foregroundColor(.green)
|
||||
.background(Circle().fill(.white).frame(width: 14, height: 14))
|
||||
.offset(x: 4, y: 4)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel(String(localized: "Mood logged: \(mood.strValue)"))
|
||||
|
||||
Text("Logged!")
|
||||
.font(.caption.weight(.semibold))
|
||||
@@ -331,8 +365,6 @@ struct VotedStatsView: View {
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel(String(localized: "Mood logged: \(entry.todaysMood?.strValue ?? "")"))
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding(12)
|
||||
@@ -340,7 +372,7 @@ struct VotedStatsView: View {
|
||||
|
||||
// MARK: - Medium: Mood + stats bar
|
||||
private var mediumLayout: some View {
|
||||
HStack(spacing: 20) {
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
if let mood = entry.todaysMood {
|
||||
// Left: Mood display
|
||||
VStack(spacing: 6) {
|
||||
@@ -349,6 +381,7 @@ struct VotedStatsView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 48, height: 48)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.accessibilityLabel(mood.strValue)
|
||||
|
||||
Text(mood.widgetDisplayName)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
@@ -359,11 +392,11 @@ struct VotedStatsView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
// Right: Stats
|
||||
// Right: Stats with progress bar aligned under title
|
||||
if let stats = entry.stats {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("\(stats.totalEntries) entries")
|
||||
.font(.caption.weight(.medium))
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
// Mini mood breakdown
|
||||
@@ -383,21 +416,21 @@ struct VotedStatsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
// Progress bar - aligned with title
|
||||
GeometryReader { geo in
|
||||
HStack(spacing: 1) {
|
||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
||||
let percentage = stats.percentage(for: mood)
|
||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { m in
|
||||
let percentage = stats.percentage(for: m)
|
||||
if percentage > 0 {
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.fill(moodTint.color(forMood: mood))
|
||||
.fill(moodTint.color(forMood: m))
|
||||
.frame(width: max(4, geo.size.width * CGFloat(percentage) / 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 8)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.frame(height: 10)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||
}
|
||||
.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()
|
||||
} timeline: {
|
||||
VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: false, todaysMood: nil, stats: nil, promptText: "How are you feeling today?")
|
||||
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: "")
|
||||
VoteWidgetEntry(date: Date(), hasSubscription: false, hasVotedToday: false, todaysMood: nil, stats: nil, promptText: "")
|
||||
VoteWidgetEntry(
|
||||
date: Date(),
|
||||
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 color: 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.date = date
|
||||
self.color = color
|
||||
self.graphic = graphic
|
||||
self.secondaryColor = secondaryColor
|
||||
self.mood = mood
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,13 +173,15 @@ struct TimeLineCreator {
|
||||
graphic: moodImages.icon(forMood: todayEntry.mood),
|
||||
date: dayStart,
|
||||
color: moodTint.color(forMood: todayEntry.mood),
|
||||
secondaryColor: moodTint.secondary(forMood: todayEntry.mood)))
|
||||
secondaryColor: moodTint.secondary(forMood: todayEntry.mood),
|
||||
mood: todayEntry.mood))
|
||||
} else {
|
||||
timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: .missing),
|
||||
graphic: moodImages.icon(forMood: .missing),
|
||||
date: dayStart,
|
||||
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),
|
||||
date: dayStart,
|
||||
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)
|
||||
.frame(width: 70, height: 70)
|
||||
.foregroundColor(today.color)
|
||||
.accessibilityLabel(today.mood.strValue)
|
||||
|
||||
Spacer()
|
||||
.frame(height: 12)
|
||||
@@ -470,7 +476,8 @@ struct MediumWidgetView: View {
|
||||
image: item.image,
|
||||
color: item.color,
|
||||
isToday: index == 0,
|
||||
height: cellHeight
|
||||
height: cellHeight,
|
||||
mood: item.mood
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -491,6 +498,7 @@ struct MediumDayCell: View {
|
||||
let color: Color
|
||||
let isToday: Bool
|
||||
let height: CGFloat
|
||||
let mood: Mood
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -500,7 +508,7 @@ struct MediumDayCell: View {
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Text(dayLabel)
|
||||
.font(.system(size: 10, weight: isToday ? .bold : .medium))
|
||||
.font(.caption2.weight(isToday ? .bold : .medium))
|
||||
.foregroundStyle(isToday ? .primary : .secondary)
|
||||
.textCase(.uppercase)
|
||||
|
||||
@@ -509,9 +517,10 @@ struct MediumDayCell: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 36, height: 36)
|
||||
.foregroundColor(color)
|
||||
.accessibilityLabel(mood.strValue)
|
||||
|
||||
Text(dateLabel)
|
||||
.font(.system(size: 13, weight: isToday ? .bold : .semibold))
|
||||
.font(.caption.weight(isToday ? .bold : .semibold))
|
||||
.foregroundStyle(isToday ? color : .secondary)
|
||||
}
|
||||
}
|
||||
@@ -584,7 +593,8 @@ struct LargeWidgetView: View {
|
||||
image: item.image,
|
||||
color: item.color,
|
||||
isToday: index == 0,
|
||||
height: cellHeight
|
||||
height: cellHeight,
|
||||
mood: item.mood
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -598,7 +608,8 @@ struct LargeWidgetView: View {
|
||||
image: item.image,
|
||||
color: item.color,
|
||||
isToday: false,
|
||||
height: cellHeight
|
||||
height: cellHeight,
|
||||
mood: item.mood
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -634,11 +645,12 @@ struct DayCell: View {
|
||||
let color: Color
|
||||
let isToday: Bool
|
||||
let height: CGFloat
|
||||
let mood: Mood
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 2) {
|
||||
Text(dayLabel)
|
||||
.font(.system(size: 10, weight: isToday ? .bold : .medium))
|
||||
.font(.caption2.weight(isToday ? .bold : .medium))
|
||||
.foregroundStyle(isToday ? .primary : .secondary)
|
||||
.textCase(.uppercase)
|
||||
|
||||
@@ -653,9 +665,10 @@ struct DayCell: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 38, height: 38)
|
||||
.foregroundColor(color)
|
||||
.accessibilityLabel(mood.strValue)
|
||||
|
||||
Text(dateLabel)
|
||||
.font(.system(size: 13, weight: isToday ? .bold : .semibold))
|
||||
.font(.caption.weight(isToday ? .bold : .semibold))
|
||||
.foregroundStyle(isToday ? color : .secondary)
|
||||
}
|
||||
}
|
||||
@@ -679,26 +692,29 @@ struct LargeVotingView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 24) {
|
||||
VStack(spacing: 16) {
|
||||
Spacer()
|
||||
|
||||
Text(hasSubscription ? promptText : "Subscribe to track your mood")
|
||||
.font(.title2.weight(.semibold))
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.8)
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
// Large mood buttons in a row
|
||||
HStack(spacing: 20) {
|
||||
// Large mood buttons in a row - flexible spacing
|
||||
HStack(spacing: 0) {
|
||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
||||
moodButton(for: mood)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -708,29 +724,35 @@ struct LargeVotingView: View {
|
||||
moodButtonContent(for: mood)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Log this mood"))
|
||||
} else {
|
||||
Link(destination: URL(string: "feels://subscribe")!) {
|
||||
moodButtonContent(for: mood)
|
||||
}
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Open app to subscribe"))
|
||||
}
|
||||
}
|
||||
|
||||
private func moodButtonContent(for mood: Mood) -> some View {
|
||||
VStack(spacing: 8) {
|
||||
VStack(spacing: 4) {
|
||||
moodImages.icon(forMood: mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 56, height: 56)
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
|
||||
Text(mood.strValue)
|
||||
.font(.caption.weight(.medium))
|
||||
Text(mood.widgetDisplayName)
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 4)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(moodTint.color(forMood: mood).opacity(0.15))
|
||||
)
|
||||
}
|
||||
@@ -899,10 +921,14 @@ struct InlineVotingView: View {
|
||||
moodIcon(for: mood)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Log this mood"))
|
||||
} else {
|
||||
Link(destination: URL(string: "feels://subscribe")!) {
|
||||
moodIcon(for: mood)
|
||||
}
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Open app to subscribe"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -924,6 +950,7 @@ struct EntryCard: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 50, height: 50, alignment: .center)
|
||||
.foregroundColor(timeLineView.color)
|
||||
.accessibilityLabel(timeLineView.mood.strValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1011,115 +1038,364 @@ struct FeelsGraphicWidget: Widget {
|
||||
|
||||
// MARK: - Preview Helpers
|
||||
|
||||
private extension FeelsWidget_Previews {
|
||||
static func sampleTimelineViews(count: Int) -> [WatchTimelineView] {
|
||||
private enum WidgetPreviewHelpers {
|
||||
static func sampleTimelineViews(count: Int, startMood: Mood = .great) -> [WatchTimelineView] {
|
||||
let moods: [Mood] = [.great, .good, .average, .bad, .horrible]
|
||||
let startIndex = moods.firstIndex(of: startMood) ?? 0
|
||||
return (0..<count).map { index in
|
||||
let mood = moods[index % moods.count]
|
||||
let mood = moods[(startIndex + index) % moods.count]
|
||||
return WatchTimelineView(
|
||||
image: EmojiMoodImages.icon(forMood: mood),
|
||||
graphic: EmojiMoodImages.icon(forMood: mood),
|
||||
date: Calendar.current.date(byAdding: .day, value: -index, to: Date())!,
|
||||
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(
|
||||
date: Date(),
|
||||
configuration: ConfigurationIntent(),
|
||||
timeLineViews: sampleTimelineViews(count: timelineCount),
|
||||
hasSubscription: true,
|
||||
hasVotedToday: true
|
||||
timeLineViews: sampleTimelineViews(count: timelineCount, startMood: startMood),
|
||||
hasSubscription: hasSubscription,
|
||||
hasVotedToday: hasVotedToday,
|
||||
promptText: "How are you feeling today?"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct FeelsWidget_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
// MARK: - FeelsWidget (Timeline)
|
||||
FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 1))
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
.previewDisplayName("Timeline - Small")
|
||||
// MARK: - FeelsWidget Previews (Timeline Widget)
|
||||
|
||||
FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 5))
|
||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||
.previewDisplayName("Timeline - Medium")
|
||||
// Small - Logged States
|
||||
#Preview("Timeline Small - Great", as: .systemSmall) {
|
||||
FeelsWidget()
|
||||
} timeline: {
|
||||
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .great)
|
||||
}
|
||||
|
||||
FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 10))
|
||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||
.previewDisplayName("Timeline - Large")
|
||||
#Preview("Timeline Small - Good", as: .systemSmall) {
|
||||
FeelsWidget()
|
||||
} timeline: {
|
||||
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .good)
|
||||
}
|
||||
|
||||
// MARK: - FeelsGraphicWidget (Mood Graphic)
|
||||
FeelsGraphicWidgetEntryView(entry: sampleEntry(timelineCount: 2))
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
.previewDisplayName("Mood Graphic - Small")
|
||||
#Preview("Timeline Small - Average", as: .systemSmall) {
|
||||
FeelsWidget()
|
||||
} timeline: {
|
||||
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .average)
|
||||
}
|
||||
|
||||
// MARK: - FeelsIconWidget (Custom Icon)
|
||||
FeelsIconWidgetEntryView(entry: sampleEntry())
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
.previewDisplayName("Custom Icon - Small")
|
||||
#Preview("Timeline Small - Bad", as: .systemSmall) {
|
||||
FeelsWidget()
|
||||
} timeline: {
|
||||
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .bad)
|
||||
}
|
||||
|
||||
// MARK: - FeelsVoteWidget (Vote - Not Voted)
|
||||
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry(
|
||||
date: Date(),
|
||||
hasSubscription: true,
|
||||
hasVotedToday: false,
|
||||
todaysMood: nil,
|
||||
stats: nil,
|
||||
promptText: "How are you feeling?"
|
||||
))
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
.previewDisplayName("Vote - Small (Not Voted)")
|
||||
#Preview("Timeline Small - Horrible", as: .systemSmall) {
|
||||
FeelsWidget()
|
||||
} timeline: {
|
||||
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .horrible)
|
||||
}
|
||||
|
||||
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry(
|
||||
date: Date(),
|
||||
hasSubscription: true,
|
||||
hasVotedToday: false,
|
||||
todaysMood: nil,
|
||||
stats: nil,
|
||||
promptText: "How are you feeling?"
|
||||
))
|
||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||
.previewDisplayName("Vote - Medium (Not Voted)")
|
||||
// Small - Voting States
|
||||
#Preview("Timeline Small - Voting", as: .systemSmall) {
|
||||
FeelsWidget()
|
||||
} timeline: {
|
||||
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, hasVotedToday: false)
|
||||
}
|
||||
|
||||
// MARK: - FeelsVoteWidget (Vote - Already Voted)
|
||||
FeelsVoteWidgetEntryView(entry: 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: ""
|
||||
))
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
.previewDisplayName("Vote - Small (Voted)")
|
||||
#Preview("Timeline Small - Non-Subscriber", as: .systemSmall) {
|
||||
FeelsWidget()
|
||||
} timeline: {
|
||||
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, hasVotedToday: false, hasSubscription: false)
|
||||
}
|
||||
|
||||
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry(
|
||||
date: Date(),
|
||||
hasSubscription: true,
|
||||
hasVotedToday: true,
|
||||
todaysMood: .good,
|
||||
stats: MoodStats(totalEntries: 45, moodCounts: [.great: 15, .good: 18, .average: 8, .bad: 3, .horrible: 1]),
|
||||
promptText: ""
|
||||
))
|
||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||
.previewDisplayName("Vote - Medium (Voted)")
|
||||
// Medium - Logged States
|
||||
#Preview("Timeline Medium - Logged", as: .systemMedium) {
|
||||
FeelsWidget()
|
||||
} timeline: {
|
||||
WidgetPreviewHelpers.sampleEntry(timelineCount: 5)
|
||||
}
|
||||
|
||||
// MARK: - FeelsVoteWidget (Non-Subscriber)
|
||||
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry(
|
||||
date: Date(),
|
||||
hasSubscription: false,
|
||||
hasVotedToday: false,
|
||||
todaysMood: nil,
|
||||
stats: nil,
|
||||
promptText: ""
|
||||
))
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
.previewDisplayName("Vote - Small (Non-Subscriber)")
|
||||
// Medium - Voting States
|
||||
#Preview("Timeline Medium - Voting", as: .systemMedium) {
|
||||
FeelsWidget()
|
||||
} timeline: {
|
||||
WidgetPreviewHelpers.sampleEntry(timelineCount: 5, hasVotedToday: false)
|
||||
}
|
||||
|
||||
#Preview("Timeline Medium - Non-Subscriber", as: .systemMedium) {
|
||||
FeelsWidget()
|
||||
} timeline: {
|
||||
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 {
|
||||
switch self {
|
||||
|
||||
case .horrible:
|
||||
return Image("HorribleGraphic", bundle: .main)
|
||||
case .bad:
|
||||
|
||||
@@ -25,6 +25,7 @@ struct OnboardingCustomizeOne: View {
|
||||
.foregroundColor(Color(UIColor.darkText))
|
||||
.opacity(0.04)
|
||||
.scaleEffect(1.2, anchor: .trailing)
|
||||
.accessibilityHidden(true)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ struct OnboardingCustomizeTwo: View {
|
||||
.foregroundColor(Color(UIColor.darkText))
|
||||
.opacity(0.04)
|
||||
.scaleEffect(1.2, anchor: .trailing)
|
||||
.accessibilityHidden(true)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
|
||||
@@ -44,21 +44,21 @@ struct OnboardingDay: View {
|
||||
.frame(width: 120, height: 120)
|
||||
|
||||
Image(systemName: "calendar")
|
||||
.font(.system(size: 44))
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.bottom, 32)
|
||||
|
||||
// Title
|
||||
Text("Which day should\nyou rate?")
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.font(.title.weight(.bold))
|
||||
.foregroundColor(.white)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
// Subtitle
|
||||
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))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
@@ -92,11 +92,11 @@ struct OnboardingDay: View {
|
||||
// Tip
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "lightbulb.fill")
|
||||
.font(.system(size: 18))
|
||||
.font(.headline)
|
||||
.foregroundColor(.yellow)
|
||||
|
||||
Text("Tip: \"Yesterday\" works great for evening reminders")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
}
|
||||
.padding(.horizontal, 30)
|
||||
@@ -124,7 +124,7 @@ struct DayOptionCard: View {
|
||||
.frame(width: 46, height: 46)
|
||||
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 20))
|
||||
.font(.title3)
|
||||
.foregroundColor(isSelected ? Color(hex: "4facfe") : .white)
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
@@ -132,15 +132,15 @@ struct DayOptionCard: View {
|
||||
// Text
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(isSelected ? Color(hex: "4facfe") : .white)
|
||||
|
||||
Text(subtitle)
|
||||
.font(.system(size: 13))
|
||||
.font(.caption)
|
||||
.foregroundColor(isSelected ? Color(hex: "4facfe").opacity(0.8) : .white.opacity(0.8))
|
||||
|
||||
Text(example)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(isSelected ? Color(hex: "4facfe").opacity(0.6) : .white.opacity(0.6))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
@@ -151,7 +151,7 @@ struct DayOptionCard: View {
|
||||
// Checkmark
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 22))
|
||||
.font(.title3)
|
||||
.foregroundColor(Color(hex: "4facfe"))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ struct OnboardingStyle: View {
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
Image(systemName: "paintpalette.fill")
|
||||
.font(.system(size: 40))
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.top, 40)
|
||||
@@ -39,13 +39,13 @@ struct OnboardingStyle: View {
|
||||
|
||||
// Title
|
||||
Text("Make it yours")
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.font(.title.weight(.bold))
|
||||
.foregroundColor(.white)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
// Subtitle
|
||||
Text("Choose your favorite style")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.font(.body.weight(.medium))
|
||||
.foregroundColor(.white.opacity(0.85))
|
||||
.padding(.bottom, 20)
|
||||
|
||||
@@ -57,7 +57,7 @@ struct OnboardingStyle: View {
|
||||
// Icon Style Section
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Icon Style")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
@@ -82,7 +82,7 @@ struct OnboardingStyle: View {
|
||||
// Color Theme Section
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Mood Colors")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
@@ -105,9 +105,9 @@ struct OnboardingStyle: View {
|
||||
// Hint
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "arrow.left.arrow.right")
|
||||
.font(.system(size: 14))
|
||||
.font(.subheadline)
|
||||
Text("You can change these anytime in Customize")
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
}
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.padding(.top, 20)
|
||||
@@ -130,14 +130,15 @@ struct OnboardingStylePreview: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(moodTint.color(forMood: .good))
|
||||
.accessibilityLabel(Mood.good.strValue)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Wednesday - 10th")
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text(Mood.good.strValue)
|
||||
.font(.system(size: 14))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
@@ -167,6 +168,7 @@ struct OnboardingIconPackOption: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.accessibilityLabel(mood.strValue)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
@@ -34,14 +34,14 @@ struct OnboardingSubscription: View {
|
||||
.frame(width: 120, height: 120)
|
||||
|
||||
Image(systemName: "crown.fill")
|
||||
.font(.system(size: 48))
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.yellow)
|
||||
}
|
||||
.padding(.bottom, 24)
|
||||
|
||||
// Title
|
||||
Text("Unlock the Full\nExperience")
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.font(.title.weight(.bold))
|
||||
.foregroundColor(.white)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom, 8)
|
||||
@@ -101,10 +101,10 @@ struct OnboardingSubscription: View {
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "sparkles")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.font(.headline.weight(.semibold))
|
||||
|
||||
Text("Get Personal Insights")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.font(.headline.weight(.bold))
|
||||
}
|
||||
.foregroundColor(Color(hex: "11998e"))
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -125,7 +125,7 @@ struct OnboardingSubscription: View {
|
||||
completionClosure(onboardingData)
|
||||
}) {
|
||||
Text("Maybe Later")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.font(.body.weight(.medium))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
.accessibilityLabel(String(localized: "Maybe Later"))
|
||||
@@ -154,18 +154,18 @@ struct BenefitRow: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 22))
|
||||
.font(.title3)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 40)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text(description)
|
||||
.font(.system(size: 13))
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
|
||||
@@ -36,21 +36,21 @@ struct OnboardingTime: View {
|
||||
.frame(width: 120, height: 120)
|
||||
|
||||
Image(systemName: "bell.fill")
|
||||
.font(.system(size: 44))
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.bottom, 32)
|
||||
|
||||
// Title
|
||||
Text("When should we\nremind you?")
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.font(.title.weight(.bold))
|
||||
.foregroundColor(.white)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
// Subtitle
|
||||
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))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
@@ -80,12 +80,12 @@ struct OnboardingTime: View {
|
||||
// Info text
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "info.circle.fill")
|
||||
.font(.system(size: 20))
|
||||
.font(.title3)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
.accessibilityHidden(true)
|
||||
|
||||
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))
|
||||
}
|
||||
.padding(.horizontal, 30)
|
||||
|
||||
@@ -23,6 +23,7 @@ struct OnboardingTitle: View {
|
||||
.opacity(0.04)
|
||||
.scaleEffect(1.2)
|
||||
.padding(.bottom, 55)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
ScrollView {
|
||||
VStack{
|
||||
@@ -37,8 +38,7 @@ struct OnboardingTitle: View {
|
||||
// onboardingData.title = option
|
||||
}, label: {
|
||||
Text(option)
|
||||
.font(.system(size: 15))
|
||||
.fontWeight(.bold)
|
||||
.font(.subheadline.weight(.bold))
|
||||
.foregroundColor(.white)
|
||||
.padding(10)
|
||||
.background(RoundedRectangle(cornerRadius: 10).stroke().foregroundColor(Color.white))
|
||||
|
||||
@@ -32,20 +32,20 @@ struct OnboardingWelcome: View {
|
||||
.frame(width: 120, height: 120)
|
||||
|
||||
Image(systemName: "heart.fill")
|
||||
.font(.system(size: 50))
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.bottom, 40)
|
||||
|
||||
// Title
|
||||
Text("Welcome to Feels")
|
||||
.font(.system(size: 34, weight: .bold, design: .rounded))
|
||||
.font(.largeTitle.weight(.bold))
|
||||
.foregroundColor(.white)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
// Subtitle
|
||||
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))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
@@ -91,18 +91,18 @@ struct FeatureRow: View {
|
||||
.frame(width: 50, height: 50)
|
||||
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 22))
|
||||
.font(.title3)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text(description)
|
||||
.font(.system(size: 14))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ struct OnboardingWrapup: View {
|
||||
.foregroundColor(Color(UIColor.darkText))
|
||||
.opacity(0.04)
|
||||
.scaleEffect(1.2, anchor: .trailing)
|
||||
.accessibilityHidden(true)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
|
||||
@@ -90,11 +90,6 @@ extension Font {
|
||||
static func scalable(_ style: Font.TextStyle, weight: Font.Weight = .regular) -> Font {
|
||||
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
|
||||
|
||||
@@ -333,7 +333,7 @@ struct AuraVotingView: View {
|
||||
|
||||
// Label with elegant typography
|
||||
Text(mood.strValue)
|
||||
.font(.system(size: 12, weight: .semibold, design: .rounded))
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(color)
|
||||
.tracking(0.5)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ struct BGViewItem: View {
|
||||
.foregroundColor(DefaultMoodTint.color(forMood: mood))
|
||||
// .blur(radius: 3)
|
||||
.opacity(0.1)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ struct CustomizeView: View {
|
||||
private var headerView: some View {
|
||||
HStack {
|
||||
Text("Customize")
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.font(.title.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Spacer()
|
||||
@@ -214,7 +214,7 @@ struct SettingsSection<Content: View>: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(title.uppercased())
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
.tracking(0.5)
|
||||
|
||||
@@ -240,7 +240,7 @@ struct SettingsRow<Content: View>: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(title)
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.7))
|
||||
|
||||
content
|
||||
@@ -272,7 +272,7 @@ struct ThemePickerCompact: View {
|
||||
|
||||
if theme == aTheme {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 18))
|
||||
.font(.headline)
|
||||
.foregroundColor(.accentColor)
|
||||
.background(Circle().fill(.white).padding(2))
|
||||
.offset(x: 14, y: 14)
|
||||
@@ -280,7 +280,7 @@ struct ThemePickerCompact: View {
|
||||
}
|
||||
|
||||
Text(aTheme.title)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(theme == aTheme ? .accentColor : textColor.opacity(0.6))
|
||||
}
|
||||
}
|
||||
@@ -310,7 +310,7 @@ struct TextColorPickerCompact: View {
|
||||
.labelsHidden()
|
||||
|
||||
Text("Sample Text")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.font(.body.weight(.medium))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Spacer()
|
||||
@@ -344,6 +344,7 @@ struct ImagePackPickerCompact: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.accessibilityLabel(mood.strValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +352,7 @@ struct ImagePackPickerCompact: View {
|
||||
|
||||
if imagePack == images {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 22))
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
@@ -398,7 +399,7 @@ struct TintPickerCompact: View {
|
||||
|
||||
if moodTint == tint {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 22))
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
@@ -432,11 +433,11 @@ struct TintPickerCompact: View {
|
||||
|
||||
if moodTint == .Custom {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 22))
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
} else {
|
||||
Text("Custom")
|
||||
.font(.system(size: 13))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -485,9 +486,13 @@ struct VotingLayoutPickerCompact: View {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
|
||||
Button(action: {
|
||||
if UIAccessibility.isReduceMotionEnabled {
|
||||
votingLayoutStyle = layout.rawValue
|
||||
} else {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
votingLayoutStyle = layout.rawValue
|
||||
}
|
||||
}
|
||||
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
|
||||
}) {
|
||||
VStack(spacing: 6) {
|
||||
@@ -496,7 +501,7 @@ struct VotingLayoutPickerCompact: View {
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.4))
|
||||
|
||||
Text(layout.displayName)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.5))
|
||||
}
|
||||
.frame(width: 70)
|
||||
@@ -608,7 +613,7 @@ struct CustomWidgetSection: View {
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
Image(systemName: "plus")
|
||||
.font(.system(size: 24, weight: .medium))
|
||||
.font(.title2.weight(.medium))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -618,9 +623,9 @@ struct CustomWidgetSection: View {
|
||||
Link(destination: URL(string: "https://support.apple.com/guide/iphone/add-widgets-iphb8f1bf206/ios")!) {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "questionmark.circle")
|
||||
.font(.system(size: 14))
|
||||
.font(.subheadline)
|
||||
Text("How to add widgets")
|
||||
.font(.system(size: 14))
|
||||
.font(.subheadline)
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
@@ -659,12 +664,12 @@ struct PersonalityPackPickerCompact: View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(String(aPack.title()))
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
let strings = aPack.randomPushNotificationStrings()
|
||||
Text(strings.body)
|
||||
.font(.system(size: 13))
|
||||
.font(.caption)
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -673,7 +678,7 @@ struct PersonalityPackPickerCompact: View {
|
||||
|
||||
if personalityPack == aPack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 22))
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
@@ -736,7 +741,7 @@ struct DayFilterPickerCompact: View {
|
||||
impactMed.impactOccurred()
|
||||
}) {
|
||||
Text(day.prefix(2).uppercased())
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(isActive ? .white : textColor.opacity(0.5))
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 40)
|
||||
@@ -750,7 +755,7 @@ struct DayFilterPickerCompact: View {
|
||||
}
|
||||
|
||||
Text(String(localized: "day_picker_view_text"))
|
||||
.font(.system(size: 13))
|
||||
.font(.caption)
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
@@ -774,15 +779,15 @@ struct SubscriptionBannerView: View {
|
||||
private var subscribedView: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.system(size: 28))
|
||||
.font(.title)
|
||||
.foregroundColor(.green)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Premium Active")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.font(.body.weight(.semibold))
|
||||
|
||||
Text("You have full access")
|
||||
.font(.system(size: 13))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
@@ -793,7 +798,7 @@ struct SubscriptionBannerView: View {
|
||||
await openSubscriptionManagement()
|
||||
}
|
||||
}
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.green)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
@@ -813,23 +818,23 @@ struct SubscriptionBannerView: View {
|
||||
}) {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "crown.fill")
|
||||
.font(.system(size: 28))
|
||||
.font(.title)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Unlock Premium")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(colorScheme == .dark ? .white : .black)
|
||||
|
||||
Text("Month & Year views, Insights & more")
|
||||
.font(.system(size: 13))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(16)
|
||||
@@ -870,9 +875,13 @@ struct DayViewStylePickerCompact: View {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(DayViewStyle.allCases, id: \.rawValue) { style in
|
||||
Button(action: {
|
||||
if UIAccessibility.isReduceMotionEnabled {
|
||||
dayViewStyle = style
|
||||
} else {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
dayViewStyle = style
|
||||
}
|
||||
}
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
||||
impactMed.impactOccurred()
|
||||
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))
|
||||
|
||||
Text(style.displayName)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.5))
|
||||
}
|
||||
.frame(width: 70)
|
||||
@@ -962,7 +971,7 @@ struct DayViewStylePickerCompact: View {
|
||||
// Giant number with glowing orb
|
||||
HStack(spacing: 4) {
|
||||
Text("17")
|
||||
.font(.system(size: 20, weight: .black, design: .rounded))
|
||||
.font(.title3.weight(.black))
|
||||
.foregroundStyle(
|
||||
LinearGradient(colors: [.green, .green.opacity(0.5)], startPoint: .top, endPoint: .bottom)
|
||||
)
|
||||
@@ -983,7 +992,7 @@ struct DayViewStylePickerCompact: View {
|
||||
Rectangle().frame(width: 34, height: 2)
|
||||
HStack(spacing: 4) {
|
||||
Text("12")
|
||||
.font(.system(size: 18, weight: .regular, design: .serif))
|
||||
.font(.headline.weight(.regular))
|
||||
Rectangle().frame(width: 1, height: 20)
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
RoundedRectangle(cornerRadius: 1).frame(width: 12, height: 3)
|
||||
@@ -1176,7 +1185,7 @@ struct DayViewStylePickerCompact: View {
|
||||
.offset(x: -6, y: 4)
|
||||
.blur(radius: 2)
|
||||
Image(systemName: "gyroscope")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
case .micro:
|
||||
|
||||
@@ -64,6 +64,8 @@ struct IconPickerView: View {
|
||||
.frame(width: 50, height:50)
|
||||
.cornerRadius(10)
|
||||
})
|
||||
.accessibilityLabel(String(localized: "Default app icon"))
|
||||
.accessibilityHint(String(localized: "Double tap to select"))
|
||||
|
||||
|
||||
ForEach(iconSets, id: \.self.0){ iconSet in
|
||||
@@ -78,6 +80,8 @@ struct IconPickerView: View {
|
||||
.frame(width: 50, height:50)
|
||||
.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()
|
||||
|
||||
@@ -31,6 +31,7 @@ struct ImagePackPickerView: View {
|
||||
.foregroundColor(
|
||||
moodTint.color(forMood: mood)
|
||||
)
|
||||
.accessibilityLabel(mood.strValue)
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
}
|
||||
|
||||
@@ -31,9 +31,13 @@ struct VotingLayoutPickerView: View {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
|
||||
Button(action: {
|
||||
if UIAccessibility.isReduceMotionEnabled {
|
||||
votingLayoutStyle = layout.rawValue
|
||||
} else {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
votingLayoutStyle = layout.rawValue
|
||||
}
|
||||
}
|
||||
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
|
||||
}) {
|
||||
VStack(spacing: 6) {
|
||||
|
||||
@@ -172,12 +172,12 @@ extension DayView {
|
||||
HStack(spacing: 10) {
|
||||
// Calendar icon
|
||||
Image(systemName: "calendar")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Spacer()
|
||||
@@ -194,7 +194,7 @@ extension DayView {
|
||||
HStack(spacing: 0) {
|
||||
// Large month number as hero element
|
||||
Text(String(format: "%02d", month))
|
||||
.font(.system(size: 48, weight: .black, design: .rounded))
|
||||
.font(.largeTitle.weight(.black))
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [textColor, textColor.opacity(0.4)],
|
||||
@@ -206,12 +206,12 @@ extension DayView {
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month).uppercased())
|
||||
.font(.system(size: 14, weight: .bold, design: .rounded))
|
||||
.font(.subheadline.weight(.bold))
|
||||
.tracking(3)
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 12, weight: .medium, design: .rounded))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
}
|
||||
|
||||
@@ -253,12 +253,12 @@ extension DayView {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||
// Large serif month name
|
||||
Text(Random.monthName(fromMonthInt: month).uppercased())
|
||||
.font(.system(size: 28, weight: .regular, design: .serif))
|
||||
.font(.title.weight(.regular))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
// Year in lighter weight
|
||||
Text(String(year))
|
||||
.font(.system(size: 16, weight: .light, design: .serif))
|
||||
.font(.body.weight(.light))
|
||||
.italic()
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
|
||||
@@ -266,7 +266,7 @@ extension DayView {
|
||||
|
||||
// Decorative flourish
|
||||
Text("§")
|
||||
.font(.system(size: 20, weight: .regular, design: .serif))
|
||||
.font(.title3.weight(.regular))
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
@@ -284,12 +284,12 @@ extension DayView {
|
||||
HStack(spacing: 12) {
|
||||
// Glowing terminal prompt
|
||||
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))
|
||||
.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))")
|
||||
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
||||
.font(.body.weight(.bold).monospaced())
|
||||
.foregroundColor(.white)
|
||||
.shadow(color: .white.opacity(0.3), radius: 2, x: 0, y: 0)
|
||||
|
||||
@@ -329,12 +329,12 @@ extension DayView {
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 18, weight: .thin))
|
||||
.font(.headline.weight(.thin))
|
||||
.tracking(4)
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 11, weight: .ultraLight))
|
||||
.font(.caption2.weight(.ultraLight))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ extension DayView {
|
||||
// Glass content
|
||||
HStack(spacing: 12) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Capsule()
|
||||
@@ -379,7 +379,7 @@ extension DayView {
|
||||
.frame(width: 4, height: 4)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.font(.body.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
|
||||
Spacer()
|
||||
@@ -405,11 +405,11 @@ extension DayView {
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("SIDE A")
|
||||
.font(.system(size: 10, weight: .bold, design: .monospaced))
|
||||
.font(.caption2.weight(.bold).monospaced())
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
|
||||
Text("\(Random.monthName(fromMonthInt: month).uppercased()) '\(String(year).suffix(2))")
|
||||
.font(.system(size: 16, weight: .black, design: .rounded))
|
||||
.font(.body.weight(.black))
|
||||
.foregroundColor(textColor)
|
||||
.tracking(1)
|
||||
}
|
||||
@@ -418,7 +418,7 @@ extension DayView {
|
||||
|
||||
// Track counter
|
||||
Text(String(format: "%02d", month))
|
||||
.font(.system(size: 20, weight: .bold, design: .monospaced))
|
||||
.font(.title3.weight(.bold).monospaced())
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
@@ -443,11 +443,11 @@ extension DayView {
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 22, weight: .light))
|
||||
.font(.title2.weight(.light))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 14, weight: .regular))
|
||||
.font(.subheadline.weight(.regular))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
|
||||
Spacer()
|
||||
@@ -483,11 +483,11 @@ extension DayView {
|
||||
.frame(width: 2)
|
||||
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 18, weight: .regular, design: .serif))
|
||||
.font(.headline.weight(.regular))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 14, weight: .light, design: .serif))
|
||||
.font(.subheadline.weight(.light))
|
||||
.italic()
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
|
||||
@@ -524,7 +524,7 @@ extension DayView {
|
||||
return HStack(spacing: 0) {
|
||||
// Month number
|
||||
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))
|
||||
.frame(width: 50)
|
||||
|
||||
@@ -554,16 +554,16 @@ extension DayView {
|
||||
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
if hasData {
|
||||
Text(String(format: "%.1f avg", averageMood + 1)) // Display as 1-5
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundColor(barColor)
|
||||
} else {
|
||||
Text(String(year))
|
||||
.font(.system(size: 11, weight: .regular))
|
||||
.font(.caption2.weight(.regular))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
}
|
||||
}
|
||||
@@ -576,11 +576,11 @@ extension DayView {
|
||||
private func patternSectionHeader(month: Int, year: Int) -> some View {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "calendar")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
|
||||
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Spacer()
|
||||
@@ -630,12 +630,12 @@ extension DayView {
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
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))
|
||||
.shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1)
|
||||
|
||||
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))
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
@@ -726,17 +726,17 @@ extension DayView {
|
||||
.blur(radius: 4)
|
||||
|
||||
Text(String(format: "%02d", month))
|
||||
.font(.system(size: 14, weight: .semibold, design: .rounded))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.font(.headline.weight(.medium))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 12, weight: .regular))
|
||||
.font(.caption.weight(.regular))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
}
|
||||
|
||||
@@ -811,18 +811,18 @@ extension DayView {
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
Image(systemName: "gyroscope")
|
||||
.font(.system(size: 22, weight: .medium))
|
||||
.font(.title2.weight(.medium))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.shadow(color: Color.purple.opacity(0.3), radius: 8, x: 0, y: 4)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
}
|
||||
|
||||
@@ -830,7 +830,7 @@ extension DayView {
|
||||
|
||||
// Tilt indicator
|
||||
Image(systemName: "iphone.gen3.radiowaves.left.and.right")
|
||||
.font(.system(size: 18))
|
||||
.font(.headline)
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
@@ -852,15 +852,15 @@ extension DayView {
|
||||
.frame(width: 3, height: 16)
|
||||
|
||||
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))
|
||||
|
||||
Text("•")
|
||||
.font(.system(size: 8))
|
||||
.font(.caption2)
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 11, weight: .medium, design: .monospaced))
|
||||
.font(.caption2.weight(.medium).monospaced())
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
|
||||
// Thin separator line
|
||||
|
||||
@@ -108,30 +108,31 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(isMissing ? .gray : .white)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
}
|
||||
.shadow(color: isMissing ? .clear : moodColor.opacity(0.4), radius: 8, x: 0, y: 4)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 6) {
|
||||
Text(Random.weekdayName(fromDate: entry.forDate))
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text("•")
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
|
||||
Text(Random.dayFormat(fromDate: entry.forDate))
|
||||
.font(.system(size: 17, weight: .medium))
|
||||
.font(.body.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.8))
|
||||
}
|
||||
|
||||
if isMissing {
|
||||
Text(String(localized: "mood_value_missing_tap_to_add"))
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(moodColor)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 4)
|
||||
@@ -145,7 +146,7 @@ struct EntryListView: View {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
@@ -184,20 +185,21 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 22, height: 22)
|
||||
.foregroundColor(isMissing ? .gray : moodColor)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide).day())
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
if isMissing {
|
||||
Text(String(localized: "mood_value_missing_tap_to_add"))
|
||||
.font(.system(size: 13))
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(moodColor)
|
||||
}
|
||||
}
|
||||
@@ -225,10 +227,10 @@ struct EntryListView: View {
|
||||
// Date column
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.textCase(.uppercase)
|
||||
}
|
||||
@@ -241,7 +243,7 @@ struct EntryListView: View {
|
||||
.frame(height: 32)
|
||||
.overlay(
|
||||
Text(String(localized: "mood_value_missing_tap_to_add"))
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(.gray)
|
||||
)
|
||||
} else {
|
||||
@@ -255,9 +257,10 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 16, height: 16)
|
||||
.foregroundColor(moodColor)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(moodColor)
|
||||
}
|
||||
)
|
||||
@@ -280,19 +283,20 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundColor(isMissing ? .gray : .white)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide).day())
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(isMissing ? textColor : .white)
|
||||
|
||||
if isMissing {
|
||||
Text(String(localized: "mood_value_missing_tap_to_add"))
|
||||
.font(.system(size: 13))
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(.white.opacity(0.85))
|
||||
}
|
||||
}
|
||||
@@ -300,7 +304,7 @@ struct EntryListView: View {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(isMissing ? textColor.opacity(0.3) : .white.opacity(0.6))
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
@@ -340,6 +344,7 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding(16)
|
||||
.foregroundColor(isMissing ? .gray : .white)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
}
|
||||
.shadow(
|
||||
color: isMissing ? .clear : moodColor.opacity(0.3),
|
||||
@@ -350,12 +355,12 @@ struct EntryListView: View {
|
||||
|
||||
// Day number
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
.font(.system(size: 16, weight: .bold, design: .rounded))
|
||||
.font(.subheadline.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
// Weekday abbreviation
|
||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.textCase(.uppercase)
|
||||
}
|
||||
@@ -372,7 +377,7 @@ struct EntryListView: View {
|
||||
HStack(spacing: 0) {
|
||||
// Giant day number - the visual hero
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
.font(.system(size: 64, weight: .black, design: .rounded))
|
||||
.font(.largeTitle.weight(.black))
|
||||
.foregroundStyle(
|
||||
isMissing
|
||||
? 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) {
|
||||
// Weekday with elegant typography
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||
.font(.system(size: 13, weight: .semibold, design: .rounded))
|
||||
.font(.caption.weight(.semibold))
|
||||
.textCase(.uppercase)
|
||||
.tracking(2)
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
@@ -423,21 +428,22 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(isMissing ? .gray : .white)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
if isMissing {
|
||||
Text(String(localized: "mood_value_missing_tap_to_add"))
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
// Month context
|
||||
Text(entry.forDate, format: .dateTime.month(.wide))
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
}
|
||||
}
|
||||
@@ -510,12 +516,12 @@ struct EntryListView: View {
|
||||
// Left column: Giant day number in serif
|
||||
VStack(alignment: .trailing, spacing: 0) {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
.font(.system(size: 72, weight: .regular, design: .serif))
|
||||
.font(.largeTitle.weight(.regular))
|
||||
.foregroundColor(textColor)
|
||||
.frame(width: 80)
|
||||
|
||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated).month(.abbreviated))
|
||||
.font(.system(size: 11, weight: .regular, design: .serif))
|
||||
.font(.caption2.weight(.regular))
|
||||
.italic()
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.textCase(.uppercase)
|
||||
@@ -530,12 +536,12 @@ struct EntryListView: View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
if isMissing {
|
||||
Text("Entry Missing")
|
||||
.font(.system(size: 24, weight: .regular, design: .serif))
|
||||
.font(.title2.weight(.regular))
|
||||
.italic()
|
||||
.foregroundColor(.gray)
|
||||
|
||||
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))
|
||||
} else {
|
||||
// Pull-quote style mood name
|
||||
@@ -545,7 +551,7 @@ struct EntryListView: View {
|
||||
.frame(width: 4)
|
||||
|
||||
Text("\"\(entry.moodString)\"")
|
||||
.font(.system(size: 28, weight: .regular, design: .serif))
|
||||
.font(.title.weight(.regular))
|
||||
.italic()
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
@@ -557,9 +563,10 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(moodColor)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
|
||||
Text("Recorded mood entry")
|
||||
.font(.system(size: 12, weight: .regular, design: .serif))
|
||||
.font(.caption.weight(.regular))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.textCase(.uppercase)
|
||||
.tracking(1.5)
|
||||
@@ -618,23 +625,24 @@ struct EntryListView: View {
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundColor(isMissing ? .gray : moodColor)
|
||||
.shadow(color: isMissing ? .clear : moodColor, radius: 8, x: 0, y: 0)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
}
|
||||
.frame(width: 52, height: 52)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
// Date in monospace terminal style
|
||||
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
|
||||
|
||||
if isMissing {
|
||||
Text("NO_DATA")
|
||||
.font(.system(size: 18, weight: .bold, design: .monospaced))
|
||||
.font(.headline.weight(.bold).monospaced())
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
// Mood in glowing text
|
||||
Text(entry.moodString.uppercased())
|
||||
.font(.system(size: 18, weight: .black, design: .default))
|
||||
.font(.headline.weight(.black))
|
||||
.foregroundColor(moodColor)
|
||||
.shadow(color: moodColor.opacity(0.8), radius: 6, x: 0, y: 0)
|
||||
.shadow(color: moodColor.opacity(0.4), radius: 12, x: 0, y: 0)
|
||||
@@ -642,7 +650,7 @@ struct EntryListView: View {
|
||||
|
||||
// Weekday
|
||||
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))
|
||||
.textCase(.uppercase)
|
||||
}
|
||||
@@ -651,7 +659,7 @@ struct EntryListView: View {
|
||||
|
||||
// Chevron with glow
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.font(.subheadline.weight(.bold))
|
||||
.foregroundColor(isMissing ? .gray : moodColor)
|
||||
.shadow(color: isMissing ? .clear : moodColor, radius: 4, x: 0, y: 0)
|
||||
}
|
||||
@@ -713,36 +721,37 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(isMissing ? .gray.opacity(0.5) : moodColor.opacity(0.8))
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// Day number with brush-like weight variation
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
.font(.system(size: 36, weight: .thin))
|
||||
.font(.title.weight(.thin))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(entry.forDate, format: .dateTime.month(.wide))
|
||||
.font(.system(size: 11, weight: .light))
|
||||
.font(.caption2.weight(.light))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.textCase(.uppercase)
|
||||
.tracking(2)
|
||||
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||
.font(.system(size: 11, weight: .light))
|
||||
.font(.caption2.weight(.light))
|
||||
.foregroundColor(textColor.opacity(0.35))
|
||||
}
|
||||
}
|
||||
|
||||
if isMissing {
|
||||
Text("—")
|
||||
.font(.system(size: 20, weight: .ultraLight))
|
||||
.font(.title3.weight(.ultraLight))
|
||||
.foregroundColor(.gray.opacity(0.4))
|
||||
} else {
|
||||
// Mood in delicate typography
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 17, weight: .light))
|
||||
.font(.body.weight(.light))
|
||||
.foregroundColor(moodColor)
|
||||
.tracking(1)
|
||||
}
|
||||
@@ -862,16 +871,17 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 26, height: 26)
|
||||
.foregroundColor(isMissing ? .gray : .white)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
|
||||
if !isMissing {
|
||||
@@ -880,11 +890,11 @@ struct EntryListView: View {
|
||||
.frame(width: 4, height: 4)
|
||||
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(moodColor)
|
||||
} else {
|
||||
Text("Tap to add")
|
||||
.font(.system(size: 13))
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
@@ -894,7 +904,7 @@ struct EntryListView: View {
|
||||
|
||||
// Prismatic chevron
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(
|
||||
isMissing
|
||||
? AnyShapeStyle(Color.gray.opacity(0.3))
|
||||
@@ -919,7 +929,7 @@ struct EntryListView: View {
|
||||
// Track number column
|
||||
VStack {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
.font(.system(size: 24, weight: .bold, design: .monospaced))
|
||||
.font(.title2.weight(.bold).monospaced())
|
||||
.foregroundColor(isMissing ? .gray : moodColor)
|
||||
}
|
||||
.frame(width: 50)
|
||||
@@ -949,17 +959,17 @@ struct EntryListView: View {
|
||||
// Track info
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
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))
|
||||
.textCase(.uppercase)
|
||||
|
||||
if isMissing {
|
||||
Text("SIDE B - NO RECORDING")
|
||||
.font(.system(size: 14, weight: .bold, design: .rounded))
|
||||
.font(.subheadline.weight(.bold))
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
Text(entry.moodString.uppercased())
|
||||
.font(.system(size: 16, weight: .black, design: .rounded))
|
||||
.font(.subheadline.weight(.black))
|
||||
.foregroundColor(textColor)
|
||||
.tracking(1)
|
||||
}
|
||||
@@ -992,6 +1002,7 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 16, height: 16)
|
||||
.foregroundColor(isMissing ? .gray : moodColor)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
@@ -1068,21 +1079,22 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundColor(.white)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// Date with organic flow
|
||||
HStack(spacing: 0) {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
.font(.system(size: 32, weight: .light))
|
||||
.font(.title.weight(.light))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(entry.forDate, format: .dateTime.month(.abbreviated))
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||
.font(.system(size: 11, weight: .regular))
|
||||
.font(.caption2.weight(.regular))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
}
|
||||
.padding(.leading, 6)
|
||||
@@ -1090,11 +1102,11 @@ struct EntryListView: View {
|
||||
|
||||
if isMissing {
|
||||
Text("No mood recorded")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundColor(moodColor)
|
||||
}
|
||||
}
|
||||
@@ -1139,11 +1151,11 @@ struct EntryListView: View {
|
||||
// Handwritten-style date
|
||||
VStack(alignment: .center, spacing: 2) {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
.font(.system(size: 36, weight: .light, design: .serif))
|
||||
.font(.title.weight(.light))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||
.font(.system(size: 12, weight: .medium, design: .serif))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.textCase(.uppercase)
|
||||
}
|
||||
@@ -1158,12 +1170,12 @@ struct EntryListView: View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// Lined paper effect
|
||||
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))
|
||||
|
||||
if isMissing {
|
||||
Text("nothing written...")
|
||||
.font(.system(size: 18, weight: .regular, design: .serif))
|
||||
.font(.headline.weight(.regular))
|
||||
.italic()
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
@@ -1173,9 +1185,10 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundColor(moodColor)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 20, weight: .medium, design: .serif))
|
||||
.font(.title3.weight(.medium))
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
}
|
||||
@@ -1209,11 +1222,11 @@ struct EntryListView: View {
|
||||
// Date column - minimal
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text(entry.forDate, format: .dateTime.day())
|
||||
.font(.system(size: 28, weight: .thin))
|
||||
.font(.title.weight(.thin))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
.textCase(.uppercase)
|
||||
}
|
||||
@@ -1263,15 +1276,16 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundColor(isMissing ? .gray : .white)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
.padding(.leading, 16)
|
||||
|
||||
if isMissing {
|
||||
Text("No entry")
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
@@ -1279,7 +1293,7 @@ struct EntryListView: View {
|
||||
|
||||
// Month indicator
|
||||
Text(entry.forDate, format: .dateTime.month(.abbreviated))
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
.font(.caption2.weight(.bold))
|
||||
.foregroundColor(isMissing ? .gray : .white.opacity(0.7))
|
||||
.padding(.trailing, 16)
|
||||
}
|
||||
@@ -1309,20 +1323,21 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundColor(.white)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide).day())
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
if isMissing {
|
||||
Text("No mood recorded")
|
||||
.font(.system(size: 14))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(moodColor)
|
||||
}
|
||||
}
|
||||
@@ -1333,7 +1348,7 @@ struct EntryListView: View {
|
||||
Spacer()
|
||||
|
||||
Text(entry.forDate, format: .dateTime.month(.abbreviated))
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
@@ -1362,6 +1377,7 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: iconSize, height: iconSize)
|
||||
.foregroundColor(isMissing ? Color.gray.opacity(0.15) : moodColor.opacity(0.2))
|
||||
.accessibilityHidden(true)
|
||||
.position(
|
||||
x: CGFloat(col) * spacing + (row.isMultiple(of: 2) ? spacing/2 : 0),
|
||||
y: CGFloat(row) * spacing
|
||||
@@ -1467,21 +1483,22 @@ struct EntryListView: View {
|
||||
.frame(width: 22, height: 22)
|
||||
.foregroundColor(.white)
|
||||
.shadow(color: Color.black.opacity(0.3), radius: 1, x: 0, y: 1)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
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))
|
||||
.shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1)
|
||||
|
||||
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))
|
||||
|
||||
if !isMissing {
|
||||
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))
|
||||
.shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1)
|
||||
}
|
||||
@@ -1598,22 +1615,23 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundColor(isMissing ? .gray : .white)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(entry.forDate, format: .dateTime.weekday(.wide))
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
HStack(spacing: 6) {
|
||||
Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
|
||||
.font(.system(size: 13))
|
||||
.font(.caption)
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
|
||||
if !isMissing {
|
||||
// Glass pill for mood
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(moodColor)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 4)
|
||||
@@ -1633,7 +1651,7 @@ struct EntryListView: View {
|
||||
|
||||
// Glass chevron
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
}
|
||||
.padding(18)
|
||||
@@ -1699,15 +1717,16 @@ struct EntryListView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(isMissing ? .gray : moodColor)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
|
||||
// Date - very compact
|
||||
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))
|
||||
|
||||
// Weekday initial
|
||||
Text(String(Random.weekdayName(fromDate: entry.forDate).prefix(3)))
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
.frame(width: 28)
|
||||
|
||||
@@ -1716,7 +1735,7 @@ struct EntryListView: View {
|
||||
// Mood as tiny pill
|
||||
if !isMissing {
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundColor(moodColor)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 3)
|
||||
@@ -1726,12 +1745,12 @@ struct EntryListView: View {
|
||||
)
|
||||
} else {
|
||||
Text("tap")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(.gray.opacity(0.6))
|
||||
}
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.25))
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
@@ -1855,12 +1874,13 @@ struct MotionCardView: View {
|
||||
x: -motionManager.xOffset * 0.3,
|
||||
y: -motionManager.yOffset * 0.3
|
||||
)
|
||||
.accessibilityLabel(entry.mood.strValue)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
// Day with motion
|
||||
Text("\(dayNumber)")
|
||||
.font(.system(size: 32, weight: .bold, design: .rounded))
|
||||
.font(.title.weight(.bold))
|
||||
.foregroundColor(isMissing ? .gray : moodColor)
|
||||
.offset(
|
||||
x: motionManager.xOffset * 0.2,
|
||||
@@ -1869,7 +1889,7 @@ struct MotionCardView: View {
|
||||
|
||||
HStack(spacing: 6) {
|
||||
Text(Random.weekdayName(fromDate: entry.forDate))
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.7))
|
||||
|
||||
if !isMissing {
|
||||
@@ -1877,7 +1897,7 @@ struct MotionCardView: View {
|
||||
.fill(moodColor)
|
||||
.frame(width: 4, height: 4)
|
||||
Text(entry.moodString)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(moodColor)
|
||||
}
|
||||
}
|
||||
@@ -1886,7 +1906,7 @@ struct MotionCardView: View {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
.offset(
|
||||
x: motionManager.xOffset * 0.5,
|
||||
@@ -1924,7 +1944,9 @@ class MotionManager: ObservableObject {
|
||||
}
|
||||
|
||||
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.startDeviceMotionUpdates(to: .main) { [weak self] motion, error in
|
||||
|
||||
@@ -28,7 +28,7 @@ struct FeelsSubscriptionStoreView: View {
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
Image(systemName: "heart.fill")
|
||||
.font(.system(size: 44))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [.pink, .red],
|
||||
@@ -40,10 +40,10 @@ struct FeelsSubscriptionStoreView: View {
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text("Unlock Premium")
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.font(.title.weight(.bold))
|
||||
|
||||
Text("Get unlimited access to all features")
|
||||
.font(.system(size: 16))
|
||||
.font(.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
@@ -80,11 +80,11 @@ struct FeatureHighlight: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 18))
|
||||
.font(.headline)
|
||||
.foregroundColor(.green)
|
||||
|
||||
Text(text)
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -26,7 +26,7 @@ struct InsightsView: View {
|
||||
// Header
|
||||
HStack {
|
||||
Text("Insights")
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.font(.title.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
Spacer()
|
||||
|
||||
@@ -34,9 +34,9 @@ struct InsightsView: View {
|
||||
if viewModel.isAIAvailable {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "sparkles")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
Text("AI")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.font(.caption.weight(.semibold))
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 8)
|
||||
@@ -118,7 +118,7 @@ struct InsightsView: View {
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
Image(systemName: "sparkles")
|
||||
.font(.system(size: 44))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [.purple, .blue],
|
||||
@@ -131,12 +131,12 @@ struct InsightsView: View {
|
||||
// Text
|
||||
VStack(spacing: 12) {
|
||||
Text("Unlock AI-Powered Insights")
|
||||
.font(.system(size: 24, weight: .bold, design: .rounded))
|
||||
.font(.title2.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
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))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
@@ -150,7 +150,7 @@ struct InsightsView: View {
|
||||
Image(systemName: "sparkles")
|
||||
Text("Get Personal Insights")
|
||||
}
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.font(.headline.weight(.bold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
@@ -202,14 +202,20 @@ struct InsightsSectionView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// 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 {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.font(.headline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
|
||||
Text(title)
|
||||
.font(.system(size: 20, weight: .bold))
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
// Loading indicator in header
|
||||
@@ -222,7 +228,7 @@ struct InsightsSectionView: View {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
@@ -277,6 +283,7 @@ struct InsightsSectionView: View {
|
||||
removal: .opacity
|
||||
))
|
||||
.animation(
|
||||
UIAccessibility.isReduceMotionEnabled ? nil :
|
||||
.spring(response: 0.4, dampingFraction: 0.8)
|
||||
.delay(Double(index) * 0.05),
|
||||
value: insights.count
|
||||
@@ -294,7 +301,7 @@ struct InsightsSectionView: View {
|
||||
.fill(colorScheme == .dark ? Color(.systemGray6) : .white)
|
||||
)
|
||||
.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)
|
||||
.animation(
|
||||
UIAccessibility.isReduceMotionEnabled ? nil :
|
||||
.easeInOut(duration: 0.8)
|
||||
.repeatForever(autoreverses: true),
|
||||
value: isAnimating
|
||||
)
|
||||
.onAppear {
|
||||
if !UIAccessibility.isReduceMotionEnabled {
|
||||
isAnimating = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Insight Card View
|
||||
|
||||
@@ -376,9 +386,10 @@ struct InsightCardView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 22, height: 22)
|
||||
.foregroundColor(accentColor)
|
||||
.accessibilityLabel(mood.strValue)
|
||||
} else {
|
||||
Image(systemName: insight.icon)
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundColor(accentColor)
|
||||
}
|
||||
}
|
||||
@@ -386,11 +397,11 @@ struct InsightCardView: View {
|
||||
// Text Content
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(insight.title)
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(insight.description)
|
||||
.font(.system(size: 14))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(textColor.opacity(0.7))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ struct LockScreenView: View {
|
||||
// App icon / lock icon
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.system(size: 60))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("Feels is Locked")
|
||||
|
||||
@@ -216,13 +216,13 @@ struct MonthCard: View {
|
||||
VStack(spacing: 0) {
|
||||
// Header with month/year
|
||||
Text("\(Random.monthName(fromMonthInt: month).uppercased()) \(String(year))")
|
||||
.font(.system(size: 32, weight: .heavy, design: .rounded))
|
||||
.font(.title.weight(.heavy))
|
||||
.foregroundColor(textColor)
|
||||
.padding(.top, 40)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text("Monthly Mood Wrap")
|
||||
.font(.system(size: 16, weight: .medium, design: .rounded))
|
||||
.font(.body.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.padding(.bottom, 30)
|
||||
|
||||
@@ -238,15 +238,16 @@ struct MonthCard: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.white)
|
||||
.padding(24)
|
||||
.accessibilityLabel(topMood.strValue)
|
||||
)
|
||||
.shadow(color: moodTint.color(forMood: topMood).opacity(0.5), radius: 20, x: 0, y: 10)
|
||||
|
||||
Text("Top Mood")
|
||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
|
||||
Text(topMood.strValue.uppercased())
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundColor(moodTint.color(forMood: topMood))
|
||||
}
|
||||
.padding(.bottom, 30)
|
||||
@@ -256,10 +257,10 @@ struct MonthCard: View {
|
||||
HStack(spacing: 0) {
|
||||
VStack(spacing: 4) {
|
||||
Text("\(totalTrackedDays)")
|
||||
.font(.system(size: 36, weight: .bold, design: .rounded))
|
||||
.font(.largeTitle.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
Text("Days Tracked")
|
||||
.font(.system(size: 12, weight: .medium, design: .rounded))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -279,6 +280,7 @@ struct MonthCard: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.white)
|
||||
.padding(7)
|
||||
.accessibilityLabel(metric.mood.strValue)
|
||||
)
|
||||
|
||||
GeometryReader { geo in
|
||||
@@ -294,7 +296,7 @@ struct MonthCard: View {
|
||||
.frame(height: 12)
|
||||
|
||||
Text("\(Int(metric.percent))%")
|
||||
.font(.system(size: 14, weight: .semibold, design: .rounded))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
.frame(width: 40, alignment: .trailing)
|
||||
}
|
||||
@@ -305,7 +307,7 @@ struct MonthCard: View {
|
||||
|
||||
// App branding
|
||||
Text("ifeel")
|
||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
@@ -317,7 +319,13 @@ struct MonthCard: View {
|
||||
VStack(spacing: 0) {
|
||||
// Month Header
|
||||
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 {
|
||||
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
|
||||
.font(.title3.bold())
|
||||
@@ -453,6 +461,7 @@ struct MoodBarChart: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(moodTint.color(forMood: metric.mood))
|
||||
.accessibilityLabel(metric.mood.strValue)
|
||||
|
||||
// Bar
|
||||
GeometryReader { geo in
|
||||
@@ -491,7 +500,7 @@ extension MonthView {
|
||||
}, label: {
|
||||
Image(systemName: "gear")
|
||||
.foregroundColor(Color(UIColor.darkGray))
|
||||
.font(.system(size: 20))
|
||||
.font(.title3)
|
||||
}).sheet(isPresented: $showingSheet) {
|
||||
SettingsView()
|
||||
}
|
||||
|
||||
@@ -277,6 +277,7 @@ struct EntryDetailView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 34, height: 34)
|
||||
.foregroundColor(.white)
|
||||
.accessibilityLabel(currentMood.strValue)
|
||||
}
|
||||
.shadow(color: moodColor.opacity(0.4), radius: 8, x: 0, y: 4)
|
||||
|
||||
@@ -304,9 +305,13 @@ struct EntryDetailView: View {
|
||||
ForEach(Mood.allValues) { mood in
|
||||
Button {
|
||||
// Update local state immediately for instant feedback
|
||||
if UIAccessibility.isReduceMotionEnabled {
|
||||
selectedMood = mood
|
||||
} else {
|
||||
withAnimation(.easeInOut(duration: 0.15)) {
|
||||
selectedMood = mood
|
||||
}
|
||||
}
|
||||
// Then persist the change
|
||||
onMoodUpdate(mood)
|
||||
} label: {
|
||||
@@ -320,6 +325,7 @@ struct EntryDetailView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundColor(currentMood == mood ? .white : .gray)
|
||||
.accessibilityLabel(mood.strValue)
|
||||
)
|
||||
|
||||
Text(mood.strValue)
|
||||
|
||||
@@ -27,7 +27,7 @@ struct PhotoPickerView: View {
|
||||
// Header
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "photo.on.rectangle.angled")
|
||||
.font(.system(size: 50))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("Add a Photo")
|
||||
@@ -281,7 +281,7 @@ struct PhotoGalleryView: View {
|
||||
} else {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "photo.badge.exclamationmark")
|
||||
.font(.system(size: 50))
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
Text("Photo not found")
|
||||
|
||||
@@ -28,7 +28,7 @@ struct SettingsTabView: View {
|
||||
VStack(spacing: 0) {
|
||||
// Header
|
||||
Text("Settings")
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.font(.title.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 16)
|
||||
@@ -92,14 +92,14 @@ struct UpgradeBannerView: View {
|
||||
// Countdown timer
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "clock")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(.orange)
|
||||
|
||||
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 {
|
||||
Text("Trial expired")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,7 @@ struct UpgradeBannerView: View {
|
||||
showWhyUpgrade = true
|
||||
} label: {
|
||||
Text("Why Upgrade?")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
@@ -126,7 +126,7 @@ struct UpgradeBannerView: View {
|
||||
showSubscriptionStore = true
|
||||
} label: {
|
||||
Text("Subscribe")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
@@ -157,7 +157,7 @@ struct WhyUpgradeView: View {
|
||||
// Header
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "star.fill")
|
||||
.font(.system(size: 50))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [.orange, .pink],
|
||||
@@ -167,7 +167,7 @@ struct WhyUpgradeView: View {
|
||||
)
|
||||
|
||||
Text("Unlock Premium")
|
||||
.font(.system(size: 28, weight: .bold))
|
||||
.font(.title.weight(.bold))
|
||||
|
||||
Text("Get the most out of your mood tracking journey")
|
||||
.font(.body)
|
||||
@@ -262,7 +262,7 @@ struct PremiumBenefitRow: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 14) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 22))
|
||||
.font(.title3)
|
||||
.foregroundColor(iconColor)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(
|
||||
@@ -272,10 +272,10 @@ struct PremiumBenefitRow: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.font(.body.weight(.semibold))
|
||||
|
||||
Text(description)
|
||||
.font(.system(size: 14))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
|
||||
@@ -174,13 +174,13 @@ struct YearCard: View {
|
||||
VStack(spacing: 0) {
|
||||
// Header with year
|
||||
Text(String(year))
|
||||
.font(.system(size: 48, weight: .heavy, design: .rounded))
|
||||
.font(.largeTitle.weight(.heavy))
|
||||
.foregroundColor(textColor)
|
||||
.padding(.top, 40)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text("Year in Review")
|
||||
.font(.system(size: 18, weight: .medium, design: .rounded))
|
||||
.font(.headline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.padding(.bottom, 30)
|
||||
|
||||
@@ -196,15 +196,16 @@ struct YearCard: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.white)
|
||||
.padding(28)
|
||||
.accessibilityLabel(topMood.strValue)
|
||||
)
|
||||
.shadow(color: moodTint.color(forMood: topMood).opacity(0.5), radius: 25, x: 0, y: 12)
|
||||
|
||||
Text("Top Mood")
|
||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
|
||||
Text(topMood.strValue.uppercased())
|
||||
.font(.system(size: 24, weight: .bold, design: .rounded))
|
||||
.font(.title2.weight(.bold))
|
||||
.foregroundColor(moodTint.color(forMood: topMood))
|
||||
}
|
||||
.padding(.bottom, 30)
|
||||
@@ -214,10 +215,10 @@ struct YearCard: View {
|
||||
HStack(spacing: 0) {
|
||||
VStack(spacing: 4) {
|
||||
Text("\(totalEntries)")
|
||||
.font(.system(size: 42, weight: .bold, design: .rounded))
|
||||
.font(.largeTitle.weight(.bold))
|
||||
.foregroundColor(textColor)
|
||||
Text("Days Tracked")
|
||||
.font(.system(size: 13, weight: .medium, design: .rounded))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -237,6 +238,7 @@ struct YearCard: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.white)
|
||||
.padding(8)
|
||||
.accessibilityLabel(metric.mood.strValue)
|
||||
)
|
||||
|
||||
GeometryReader { geo in
|
||||
@@ -252,7 +254,7 @@ struct YearCard: View {
|
||||
.frame(height: 16)
|
||||
|
||||
Text("\(Int(metric.percent))%")
|
||||
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundColor(textColor)
|
||||
.frame(width: 45, alignment: .trailing)
|
||||
}
|
||||
@@ -263,7 +265,7 @@ struct YearCard: View {
|
||||
|
||||
// App branding
|
||||
Text("ifeel")
|
||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
@@ -275,7 +277,13 @@ struct YearCard: View {
|
||||
VStack(spacing: 0) {
|
||||
// Year Header
|
||||
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 {
|
||||
Text(String(year))
|
||||
.font(.title2.bold())
|
||||
@@ -324,6 +332,7 @@ struct YearCard: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 16, height: 16)
|
||||
.foregroundColor(moodTint.color(forMood: metric.mood))
|
||||
.accessibilityLabel(metric.mood.strValue)
|
||||
|
||||
GeometryReader { geo in
|
||||
ZStack(alignment: .leading) {
|
||||
@@ -357,7 +366,7 @@ struct YearCard: View {
|
||||
HStack(spacing: 2) {
|
||||
ForEach(months.indices, id: \.self) { index in
|
||||
Text(months[index])
|
||||
.font(.system(size: 9, weight: .medium))
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.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(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(7) .feature-icon { background: rgba(94, 186, 175, 0.2); }
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 1.25rem;
|
||||
@@ -1191,6 +1192,12 @@
|
||||
<h3>Private by Design</h3>
|
||||
<p>Your feelings are yours alone. All data stays on your devices with iCloud sync you control.</p>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1107,6 +1107,12 @@
|
||||
<h3>Privacy</h3>
|
||||
<p>Your data stays on your devices. iCloud sync with no third-party access.</p>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -491,6 +491,7 @@
|
||||
.feature-icon.purple { background: rgba(168, 85, 247, 0.15); }
|
||||
.feature-icon.red { background: rgba(248, 113, 113, 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 {
|
||||
font-size: 1.125rem;
|
||||
@@ -956,6 +957,11 @@
|
||||
<h3>Privacy First</h3>
|
||||
<p>Your feelings stay yours. All data lives on your devices with iCloud sync.</p>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user