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:
Trey t
2025-12-24 09:53:40 -06:00
parent 5f7d909d62
commit be84825aba
33 changed files with 10467 additions and 9725 deletions

View File

@@ -1,7 +1,9 @@
{ {
"sourceLanguage" : "en", "sourceLanguage" : "en",
"strings" : { "strings" : {
"": {}, "" : {
},
" " : { " " : {
"comment" : "A placeholder text used to create spacing between list items.", "comment" : "A placeholder text used to create spacing between list items.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@@ -122,18 +124,18 @@
"comment" : "The month and year displayed in the header of the calendar view. The first argument is the name of the month. The second argument is the last two digits of the year.", "comment" : "The month and year displayed in the header of the calendar view. The first argument is the name of the month. The second argument is the last two digits of the year.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
"localizations" : { "localizations" : {
"en": {
"stringUnit": {
"state": "new",
"value": "%1$@ '%2$@"
}
},
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "%1$@ '%2$@" "value" : "%1$@ '%2$@"
} }
}, },
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ '%2$@"
}
},
"es" : { "es" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
@@ -170,18 +172,18 @@
"comment" : "A month and year label displayed in a calendar view. The first argument is the name of the month. The second argument is the year.", "comment" : "A month and year label displayed in a calendar view. The first argument is the name of the month. The second argument is the year.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
"localizations" : { "localizations" : {
"en": {
"stringUnit": {
"state": "new",
"value": "%1$@ %2$@"
}
},
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "%1$@ %2$@" "value" : "%1$@ %2$@"
} }
}, },
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ %2$@"
}
},
"es" : { "es" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
@@ -218,18 +220,18 @@
"comment" : "A label that combines the mood description with the date it was recorded, separated by a space.", "comment" : "A label that combines the mood description with the date it was recorded, separated by a space.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
"localizations" : { "localizations" : {
"en": {
"stringUnit": {
"state": "new",
"value": "%1$@ on %2$@"
}
},
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "%1$@ am %2$@" "value" : "%1$@ am %2$@"
} }
}, },
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ on %2$@"
}
},
"es" : { "es" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
@@ -266,18 +268,18 @@
"comment" : "The current version of the app, displayed in a small, secondary font.", "comment" : "The current version of the app, displayed in a small, secondary font.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
"localizations" : { "localizations" : {
"en": {
"stringUnit": {
"state": "new",
"value": "%1$@ v %2$@ (Build %3$@)"
}
},
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "%1$@ v %2$@ (Build %3$@)" "value" : "%1$@ v %2$@ (Build %3$@)"
} }
}, },
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ v %2$@ (Build %3$@)"
}
},
"es" : { "es" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
@@ -322,6 +324,34 @@
} }
} }
}, },
"%@, %@" : {
"comment" : "A button that, when tapped, selects or deselects a day option. The button's label is a combination of the title and subtitle.",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@, %2$@"
}
}
}
},
"%@, no mood logged" : {
"comment" : "A string that describes a day with no mood logged. The argument is the date string.",
"isCommentAutoGenerated" : true
},
"%@: %@" : {
"comment" : "An element that represents a benefit of a service or product, with a title and a description.",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@: %2$@"
}
}
}
},
"%@%@" : { "%@%@" : {
"comment" : "A text that displays the number of days remaining in the trial period, prefixed by \"Trial expires in \". The text is displayed in bold when the trial period is nearing its end.", "comment" : "A text that displays the number of days remaining in the trial period, prefixed by \"Trial expires in \". The text is displayed in bold when the trial period is nearing its end.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -464,22 +494,26 @@
} }
} }
}, },
"%lld percent" : {
"comment" : "A value indicating the percentage of health data that has been successfully synced with Apple Health.",
"isCommentAutoGenerated" : true
},
"%lld/%lld" : { "%lld/%lld" : {
"comment" : "A text view showing the current number of characters in the note, followed by a slash and the maximum allowed number of characters.", "comment" : "A text view showing the current number of characters in the note, followed by a slash and the maximum allowed number of characters.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
"localizations" : { "localizations" : {
"en": {
"stringUnit": {
"state": "new",
"value": "%1$lld/%2$lld"
}
},
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "%1$lld/%2$lld" "value" : "%1$lld/%2$lld"
} }
}, },
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$lld/%2$lld"
}
},
"es" : { "es" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
@@ -524,8 +558,12 @@
"comment" : "A symbol that appears before a command in a terminal interface.", "comment" : "A symbol that appears before a command in a terminal interface.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"12": {}, "12" : {
"17": {},
},
"17" : {
},
"20" : { "20" : {
"comment" : "A placeholder text that appears in place of a number.", "comment" : "A placeholder text that appears in place of a number.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@@ -1049,6 +1087,10 @@
} }
} }
}, },
"Allow deleting mood entries by swiping" : {
"comment" : "A hint describing the functionality of the \"Allow deleting mood entries by swiping\" toggle.",
"isCommentAutoGenerated" : true
},
"Amazing! You have a %lld day streak. Keep it up!" : { "Amazing! You have a %lld day streak. Keep it up!" : {
"comment" : "A response to a voice intent that confirms a user's current mood logging streak. The argument is the length of the streak.", "comment" : "A response to a voice intent that confirms a user's current mood logging streak. The argument is the length of the streak.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -1091,6 +1133,10 @@
} }
} }
}, },
"App icon style %@" : {
"comment" : "A button that lets the user select an app icon style. The label shows the name of the style, without the \"AppIcon\" or \"Image\" prefix.",
"isCommentAutoGenerated" : true
},
"Apple Health" : { "Apple Health" : {
"comment" : "The title of the toggle that enables or disables syncing mood data with Apple Health.", "comment" : "The title of the toggle that enables or disables syncing mood data with Apple Health.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -1133,6 +1179,10 @@
} }
} }
}, },
"Apple Health not available" : {
"comment" : "An accessibility label for the section of the settings view that indicates that Apple Health is not available on the user's device.",
"isCommentAutoGenerated" : true
},
"Are you sure you want to delete this mood entry? This cannot be undone." : { "Are you sure you want to delete this mood entry? This cannot be undone." : {
"comment" : "An alert message displayed when the user attempts to delete a mood entry.", "comment" : "An alert message displayed when the user attempts to delete a mood entry.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -3529,6 +3579,10 @@
} }
} }
}, },
"Default app icon" : {
"comment" : "A description of the default app icon option in the icon picker.",
"isCommentAutoGenerated" : true
},
"default_notif_body_today_four" : { "default_notif_body_today_four" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -4424,6 +4478,10 @@
} }
} }
}, },
"Double tap to select" : {
"comment" : "A hint that appears when tapping on an icon to select it.",
"isCommentAutoGenerated" : true
},
"Edit" : { "Edit" : {
"comment" : "A button label that triggers the editing of a journal note.", "comment" : "A button label that triggers the editing of a journal note.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -4844,6 +4902,10 @@
} }
} }
}, },
"Export your mood data as CSV or PDF" : {
"comment" : "A hint that describes the functionality of the \"Export Data\" button in the Settings view.",
"isCommentAutoGenerated" : true
},
"Exporting..." : { "Exporting..." : {
"comment" : "A label indicating that a mood data export is in progress.", "comment" : "A label indicating that a mood data export is in progress.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -6115,6 +6177,9 @@
} }
} }
} }
},
"Log this mood" : {
}, },
"Log your mood daily to build a streak. Consistency helps you understand your patterns." : { "Log your mood daily to build a streak. Consistency helps you understand your patterns." : {
"localizations" : { "localizations" : {
@@ -6576,6 +6641,14 @@
} }
} }
}, },
"Mood logged: %@" : {
"comment" : "A label describing the logged mood. The argument is the logged mood.",
"isCommentAutoGenerated" : true
},
"Mood selection" : {
"comment" : "A heading for the mood selection section.",
"isCommentAutoGenerated" : true
},
"Mood Streak" : { "Mood Streak" : {
"comment" : "Title of an app shortcut that shows the user their mood streak.", "comment" : "Title of an app shortcut that shows the user their mood streak.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -8369,6 +8442,10 @@
} }
} }
}, },
"Open app to subscribe" : {
"comment" : "A hint that appears when a user taps on a mood button in the voting view, explaining that they need to open the app to subscribe.",
"isCommentAutoGenerated" : true
},
"Open Feels" : { "Open Feels" : {
"comment" : "Title of the app intent to open the Feels app.", "comment" : "Title of the app intent to open the Feels app.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -8495,6 +8572,22 @@
} }
} }
}, },
"Opens End User License Agreement in browser" : {
"comment" : "A button that opens the app's End User License Agreement in the user's web browser.",
"isCommentAutoGenerated" : true
},
"Opens Privacy Policy in browser" : {
"comment" : "A button that opens the app's privacy policy in a web browser.",
"isCommentAutoGenerated" : true
},
"Opens subscription options" : {
"comment" : "A hint for a button that opens a subscription store.",
"isCommentAutoGenerated" : true
},
"Opens time picker to change reminder time" : {
"comment" : "A hint that describes the action of tapping the \"Reminder Time\" button in the Settings view.",
"isCommentAutoGenerated" : true
},
"Or use your device passcode" : { "Or use your device passcode" : {
"comment" : "A hint displayed below the \"Unlock with biometric\" button, encouraging users to use their device passcode if they don't have a biometric authentication method set up.", "comment" : "A hint displayed below the \"Unlock with biometric\" button, encouraging users to use their device passcode if they don't have a biometric authentication method set up.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -8871,6 +8964,10 @@
} }
} }
}, },
"Premium feature, subscription required" : {
"comment" : "A description of a premium feature that requires a subscription.",
"isCommentAutoGenerated" : true
},
"Privacy Lock" : { "Privacy Lock" : {
"comment" : "A title for a toggle that controls whether or not biometric authentication is enabled.", "comment" : "A title for a toggle that controls whether or not biometric authentication is enabled.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -9823,6 +9920,10 @@
} }
} }
}, },
"Reminder time" : {
"comment" : "An accessibility label for the time picker in the onboarding flow.",
"isCommentAutoGenerated" : true
},
"Reminder Time" : { "Reminder Time" : {
"comment" : "A label displayed above the reminder time in the settings view.", "comment" : "A label displayed above the reminder time in the settings view.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -9949,6 +10050,10 @@
} }
} }
}, },
"Require biometric authentication to open app" : {
"comment" : "A hint that describes the purpose of the Privacy Lock toggle.",
"isCommentAutoGenerated" : true
},
"Reset luanch date to current date" : { "Reset luanch date to current date" : {
"comment" : "A button label that resets the app's launch date to the current date.", "comment" : "A button label that resets the app's launch date to the current date.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -10676,6 +10781,14 @@
} }
} }
}, },
"Select this mood" : {
"comment" : "A hint that appears when a user taps on a mood button.",
"isCommentAutoGenerated" : true
},
"Select when you want to be reminded" : {
"comment" : "A hint that appears when a user taps on the time picker in the \"Reminder time\" section of the OnboardingTime view.",
"isCommentAutoGenerated" : true
},
"Set Trial Start Date" : { "Set Trial Start Date" : {
"comment" : "The title of a screen that lets a user set the start date of a free trial.", "comment" : "The title of a screen that lets a user set the start date of a free trial.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -11450,6 +11563,10 @@
} }
} }
}, },
"Skip subscription and complete setup" : {
"comment" : "A button label that says \"Skip subscription and complete setup\". It's used in the \"OnboardingSubscription\" view.",
"isCommentAutoGenerated" : true
},
"Streak: %lld days" : { "Streak: %lld days" : {
"comment" : "A label in the expanded view that describes the current streak of days the user has logged in.", "comment" : "A label in the expanded view that describes the current streak of days the user has logged in.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -11811,6 +11928,10 @@
} }
} }
}, },
"Swipe right to continue" : {
"comment" : "A hint that appears when a user swipes to the next onboarding step.",
"isCommentAutoGenerated" : true
},
"Swipe to get started" : { "Swipe to get started" : {
"comment" : "A hint displayed below the feature rows, instructing users to swipe to continue.", "comment" : "A hint displayed below the feature rows, instructing users to swipe to continue.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -11853,6 +11974,10 @@
} }
} }
}, },
"Swipe to the next onboarding step" : {
"comment" : "An accessibility hint that describes the action to take to progress to the next onboarding step.",
"isCommentAutoGenerated" : true
},
"Switch between Day, Month, and Year views to see your mood patterns over time." : { "Switch between Day, Month, and Year views to see your mood patterns over time." : {
"comment" : "A tip that instructs the user to switch between different time views to view their mood history.", "comment" : "A tip that instructs the user to switch between different time views to view their mood history.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -11895,6 +12020,10 @@
} }
} }
}, },
"Sync mood data with Apple Health" : {
"comment" : "A hint that appears when the user taps the toggle to sync mood data with Apple Health.",
"isCommentAutoGenerated" : true
},
"Sync with Apple Health" : { "Sync with Apple Health" : {
"comment" : "A tip to sync their data with Apple Health.", "comment" : "A tip to sync their data with Apple Health.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -11937,6 +12066,10 @@
} }
} }
}, },
"Syncing health data" : {
"comment" : "A label indicating that health data is being synced.",
"isCommentAutoGenerated" : true
},
"Take Photo" : { "Take Photo" : {
"comment" : "A button that takes a photo using the device's camera.", "comment" : "A button that takes a photo using the device's camera.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -12147,6 +12280,10 @@
} }
} }
}, },
"Tap to log mood for this day" : {
"comment" : "A hint that appears when a user taps on a day with no mood logged, instructing them to log a mood.",
"isCommentAutoGenerated" : true
},
"Tap to log your mood" : { "Tap to log your mood" : {
"comment" : "A description of an action a user can take to log their mood.", "comment" : "A description of an action a user can take to log their mood.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -12189,6 +12326,10 @@
} }
} }
}, },
"Tap to open app and subscribe" : {
"comment" : "A hint that describes the action to subscribe to the widget.",
"isCommentAutoGenerated" : true
},
"Tap to record your mood for this day" : { "Tap to record your mood for this day" : {
"comment" : "A description of what a user can do to add a new entry to their mood journal.", "comment" : "A description of what a user can do to add a new entry to their mood journal.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -12273,6 +12414,10 @@
} }
} }
}, },
"Tap to view or edit" : {
"comment" : "A hint that appears when a user taps on an entry to view or edit it.",
"isCommentAutoGenerated" : true
},
"Test builds only" : { "Test builds only" : {
"comment" : "A section header that indicates that the settings view contains only test data.", "comment" : "A section header that indicates that the settings view contains only test data.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -13151,6 +13296,10 @@
} }
} }
}, },
"View the app introduction again" : {
"comment" : "A button that allows a user to view the app's introductory screen again.",
"isCommentAutoGenerated" : true
},
"View Your History" : { "View Your History" : {
"comment" : "A tip title for viewing and managing one's mood history.", "comment" : "A tip title for viewing and managing one's mood history.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,

View File

@@ -227,31 +227,31 @@ struct VotingView: View {
// MARK: - Medium Widget: Single row // MARK: - Medium Widget: Single row
private var mediumLayout: some View { private var mediumLayout: some View {
VStack { VStack(spacing: 12) {
Text(hasSubscription ? promptText : "Subscribe to track your mood") Text(hasSubscription ? promptText : "Subscribe to track your mood")
.font(.headline) .font(.headline)
.foregroundStyle(.primary) .foregroundStyle(.primary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.lineLimit(2) .lineLimit(2)
.minimumScaleFactor(0.8) .minimumScaleFactor(0.8)
.padding(.bottom, 20)
HStack(spacing: 16) { HStack(spacing: 0) {
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
moodButton(for: mood, size: 44) moodButtonMedium(for: mood)
.frame(maxWidth: .infinity)
} }
} }
} }
.padding() .padding(.horizontal, 12)
.padding(.vertical, 16)
} }
@ViewBuilder @ViewBuilder
private func moodButton(for mood: Mood, size: CGFloat) -> some View { private func moodButton(for mood: Mood, size: CGFloat) -> some View {
// Ensure minimum 44x44 touch target for accessibility // Used for small widget
let touchSize = max(size, 44) let touchSize = max(size, 44)
if hasSubscription { if hasSubscription {
// Active subscription: vote normally
Button(intent: VoteMoodIntent(mood: mood)) { Button(intent: VoteMoodIntent(mood: mood)) {
moodIcon(for: mood, size: size) moodIcon(for: mood, size: size)
.frame(minWidth: touchSize, minHeight: touchSize) .frame(minWidth: touchSize, minHeight: touchSize)
@@ -260,7 +260,6 @@ struct VotingView: View {
.accessibilityLabel(mood.strValue) .accessibilityLabel(mood.strValue)
.accessibilityHint(String(localized: "Log this mood")) .accessibilityHint(String(localized: "Log this mood"))
} else { } else {
// Trial expired: open app to subscribe
Link(destination: URL(string: "feels://subscribe")!) { Link(destination: URL(string: "feels://subscribe")!) {
moodIcon(for: mood, size: size) moodIcon(for: mood, size: size)
.frame(minWidth: touchSize, minHeight: touchSize) .frame(minWidth: touchSize, minHeight: touchSize)
@@ -270,6 +269,39 @@ struct VotingView: View {
} }
} }
@ViewBuilder
private func moodButtonMedium(for mood: Mood) -> some View {
// Medium widget uses smaller icons with labels, flexible width
let content = VStack(spacing: 4) {
moodImages.icon(forMood: mood)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 32, height: 32)
.foregroundColor(moodTint.color(forMood: mood))
Text(mood.widgetDisplayName)
.font(.caption2)
.foregroundColor(moodTint.color(forMood: mood))
.lineLimit(1)
.minimumScaleFactor(0.8)
}
if hasSubscription {
Button(intent: VoteMoodIntent(mood: mood)) {
content
}
.buttonStyle(.plain)
.accessibilityLabel(mood.strValue)
.accessibilityHint(String(localized: "Log this mood"))
} else {
Link(destination: URL(string: "feels://subscribe")!) {
content
}
.accessibilityLabel(mood.strValue)
.accessibilityHint(String(localized: "Open app to subscribe"))
}
}
private func moodIcon(for mood: Mood, size: CGFloat) -> some View { private func moodIcon(for mood: Mood, size: CGFloat) -> some View {
moodImages.icon(forMood: mood) moodImages.icon(forMood: mood)
.resizable() .resizable()
@@ -315,11 +347,13 @@ struct VotedStatsView: View {
// Checkmark badge // Checkmark badge
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.font(.system(size: 18)) .font(.headline)
.foregroundColor(.green) .foregroundColor(.green)
.background(Circle().fill(.white).frame(width: 14, height: 14)) .background(Circle().fill(.white).frame(width: 14, height: 14))
.offset(x: 4, y: 4) .offset(x: 4, y: 4)
} }
.accessibilityElement(children: .combine)
.accessibilityLabel(String(localized: "Mood logged: \(mood.strValue)"))
Text("Logged!") Text("Logged!")
.font(.caption.weight(.semibold)) .font(.caption.weight(.semibold))
@@ -331,8 +365,6 @@ struct VotedStatsView: View {
.foregroundStyle(.tertiary) .foregroundStyle(.tertiary)
} }
} }
.accessibilityElement(children: .combine)
.accessibilityLabel(String(localized: "Mood logged: \(entry.todaysMood?.strValue ?? "")"))
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(12) .padding(12)
@@ -340,7 +372,7 @@ struct VotedStatsView: View {
// MARK: - Medium: Mood + stats bar // MARK: - Medium: Mood + stats bar
private var mediumLayout: some View { private var mediumLayout: some View {
HStack(spacing: 20) { HStack(alignment: .top, spacing: 20) {
if let mood = entry.todaysMood { if let mood = entry.todaysMood {
// Left: Mood display // Left: Mood display
VStack(spacing: 6) { VStack(spacing: 6) {
@@ -349,6 +381,7 @@ struct VotedStatsView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 48, height: 48) .frame(width: 48, height: 48)
.foregroundColor(moodTint.color(forMood: mood)) .foregroundColor(moodTint.color(forMood: mood))
.accessibilityLabel(mood.strValue)
Text(mood.widgetDisplayName) Text(mood.widgetDisplayName)
.font(.subheadline.weight(.semibold)) .font(.subheadline.weight(.semibold))
@@ -359,11 +392,11 @@ struct VotedStatsView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
// Right: Stats // Right: Stats with progress bar aligned under title
if let stats = entry.stats { if let stats = entry.stats {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 10) {
Text("\(stats.totalEntries) entries") Text("\(stats.totalEntries) entries")
.font(.caption.weight(.medium)) .font(.headline.weight(.semibold))
.foregroundStyle(.primary) .foregroundStyle(.primary)
// Mini mood breakdown // Mini mood breakdown
@@ -383,21 +416,21 @@ struct VotedStatsView: View {
} }
} }
// Progress bar // Progress bar - aligned with title
GeometryReader { geo in GeometryReader { geo in
HStack(spacing: 1) { HStack(spacing: 1) {
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { m in
let percentage = stats.percentage(for: mood) let percentage = stats.percentage(for: m)
if percentage > 0 { if percentage > 0 {
RoundedRectangle(cornerRadius: 2) RoundedRectangle(cornerRadius: 2)
.fill(moodTint.color(forMood: mood)) .fill(moodTint.color(forMood: m))
.frame(width: max(4, geo.size.width * CGFloat(percentage) / 100)) .frame(width: max(4, geo.size.width * CGFloat(percentage) / 100))
} }
} }
} }
} }
.frame(height: 8) .frame(height: 10)
.clipShape(RoundedRectangle(cornerRadius: 4)) .clipShape(RoundedRectangle(cornerRadius: 5))
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
@@ -449,12 +482,202 @@ struct FeelsVoteWidget: Widget {
} }
} }
// MARK: - Preview // MARK: - Preview Helpers
#Preview(as: .systemSmall) { private enum VoteWidgetPreviewHelpers {
static let sampleStats = MoodStats(
totalEntries: 30,
moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]
)
static let largeStats = MoodStats(
totalEntries: 100,
moodCounts: [.great: 35, .good: 40, .average: 15, .bad: 7, .horrible: 3]
)
}
// MARK: - Small Widget Previews
#Preview("Vote Small - Not Voted", as: .systemSmall) {
FeelsVoteWidget() FeelsVoteWidget()
} timeline: { } timeline: {
VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: false, todaysMood: nil, stats: nil, promptText: "How are you feeling today?") VoteWidgetEntry(
VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .great, stats: MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]), promptText: "") date: Date(),
VoteWidgetEntry(date: Date(), hasSubscription: false, hasVotedToday: false, todaysMood: nil, stats: nil, promptText: "") hasSubscription: true,
hasVotedToday: false,
todaysMood: nil,
stats: nil,
promptText: "How are you feeling today?"
)
}
#Preview("Vote Small - Voted Great", as: .systemSmall) {
FeelsVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .great,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Small - Voted Good", as: .systemSmall) {
FeelsVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .good,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Small - Voted Average", as: .systemSmall) {
FeelsVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .average,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Small - Voted Bad", as: .systemSmall) {
FeelsVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .bad,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Small - Voted Horrible", as: .systemSmall) {
FeelsVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .horrible,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Small - Non-Subscriber", as: .systemSmall) {
FeelsVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
hasSubscription: false,
hasVotedToday: false,
todaysMood: nil,
stats: nil,
promptText: ""
)
}
// MARK: - Medium Widget Previews
#Preview("Vote Medium - Not Voted", as: .systemMedium) {
FeelsVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
hasSubscription: true,
hasVotedToday: false,
todaysMood: nil,
stats: nil,
promptText: "How are you feeling today?"
)
}
#Preview("Vote Medium - Voted Great", as: .systemMedium) {
FeelsVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .great,
stats: VoteWidgetPreviewHelpers.largeStats,
promptText: ""
)
}
#Preview("Vote Medium - Voted Good", as: .systemMedium) {
FeelsVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .good,
stats: VoteWidgetPreviewHelpers.largeStats,
promptText: ""
)
}
#Preview("Vote Medium - Voted Average", as: .systemMedium) {
FeelsVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .average,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Medium - Voted Bad", as: .systemMedium) {
FeelsVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .bad,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Medium - Voted Horrible", as: .systemMedium) {
FeelsVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
hasSubscription: true,
hasVotedToday: true,
todaysMood: .horrible,
stats: VoteWidgetPreviewHelpers.sampleStats,
promptText: ""
)
}
#Preview("Vote Medium - Non-Subscriber", as: .systemMedium) {
FeelsVoteWidget()
} timeline: {
VoteWidgetEntry(
date: Date(),
hasSubscription: false,
hasVotedToday: false,
todaysMood: nil,
stats: nil,
promptText: ""
)
} }

View File

@@ -138,13 +138,15 @@ class WatchTimelineView: Identifiable {
let date: Date let date: Date
let color: Color let color: Color
let secondaryColor: Color let secondaryColor: Color
let mood: Mood
init(image: Image, graphic: Image, date: Date, color: Color, secondaryColor: Color) { init(image: Image, graphic: Image, date: Date, color: Color, secondaryColor: Color, mood: Mood) {
self.image = image self.image = image
self.date = date self.date = date
self.color = color self.color = color
self.graphic = graphic self.graphic = graphic
self.secondaryColor = secondaryColor self.secondaryColor = secondaryColor
self.mood = mood
} }
} }
@@ -171,13 +173,15 @@ struct TimeLineCreator {
graphic: moodImages.icon(forMood: todayEntry.mood), graphic: moodImages.icon(forMood: todayEntry.mood),
date: dayStart, date: dayStart,
color: moodTint.color(forMood: todayEntry.mood), color: moodTint.color(forMood: todayEntry.mood),
secondaryColor: moodTint.secondary(forMood: todayEntry.mood))) secondaryColor: moodTint.secondary(forMood: todayEntry.mood),
mood: todayEntry.mood))
} else { } else {
timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: .missing), timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: .missing),
graphic: moodImages.icon(forMood: .missing), graphic: moodImages.icon(forMood: .missing),
date: dayStart, date: dayStart,
color: moodTint.color(forMood: .missing), color: moodTint.color(forMood: .missing),
secondaryColor: moodTint.secondary(forMood: .missing))) secondaryColor: moodTint.secondary(forMood: .missing),
mood: .missing))
} }
} }
@@ -202,7 +206,8 @@ struct TimeLineCreator {
graphic: moodImages.icon(forMood: mood), graphic: moodImages.icon(forMood: mood),
date: dayStart, date: dayStart,
color: moodTint.color(forMood: mood), color: moodTint.color(forMood: mood),
secondaryColor: moodTint.secondary(forMood: mood) secondaryColor: moodTint.secondary(forMood: mood),
mood: mood
)) ))
} }
@@ -377,6 +382,7 @@ struct SmallWidgetView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 70, height: 70) .frame(width: 70, height: 70)
.foregroundColor(today.color) .foregroundColor(today.color)
.accessibilityLabel(today.mood.strValue)
Spacer() Spacer()
.frame(height: 12) .frame(height: 12)
@@ -470,7 +476,8 @@ struct MediumWidgetView: View {
image: item.image, image: item.image,
color: item.color, color: item.color,
isToday: index == 0, isToday: index == 0,
height: cellHeight height: cellHeight,
mood: item.mood
) )
} }
} }
@@ -491,6 +498,7 @@ struct MediumDayCell: View {
let color: Color let color: Color
let isToday: Bool let isToday: Bool
let height: CGFloat let height: CGFloat
let mood: Mood
var body: some View { var body: some View {
ZStack { ZStack {
@@ -500,7 +508,7 @@ struct MediumDayCell: View {
VStack(spacing: 4) { VStack(spacing: 4) {
Text(dayLabel) Text(dayLabel)
.font(.system(size: 10, weight: isToday ? .bold : .medium)) .font(.caption2.weight(isToday ? .bold : .medium))
.foregroundStyle(isToday ? .primary : .secondary) .foregroundStyle(isToday ? .primary : .secondary)
.textCase(.uppercase) .textCase(.uppercase)
@@ -509,9 +517,10 @@ struct MediumDayCell: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 36, height: 36) .frame(width: 36, height: 36)
.foregroundColor(color) .foregroundColor(color)
.accessibilityLabel(mood.strValue)
Text(dateLabel) Text(dateLabel)
.font(.system(size: 13, weight: isToday ? .bold : .semibold)) .font(.caption.weight(isToday ? .bold : .semibold))
.foregroundStyle(isToday ? color : .secondary) .foregroundStyle(isToday ? color : .secondary)
} }
} }
@@ -584,7 +593,8 @@ struct LargeWidgetView: View {
image: item.image, image: item.image,
color: item.color, color: item.color,
isToday: index == 0, isToday: index == 0,
height: cellHeight height: cellHeight,
mood: item.mood
) )
} }
} }
@@ -598,7 +608,8 @@ struct LargeWidgetView: View {
image: item.image, image: item.image,
color: item.color, color: item.color,
isToday: false, isToday: false,
height: cellHeight height: cellHeight,
mood: item.mood
) )
} }
} }
@@ -634,11 +645,12 @@ struct DayCell: View {
let color: Color let color: Color
let isToday: Bool let isToday: Bool
let height: CGFloat let height: CGFloat
let mood: Mood
var body: some View { var body: some View {
VStack(spacing: 2) { VStack(spacing: 2) {
Text(dayLabel) Text(dayLabel)
.font(.system(size: 10, weight: isToday ? .bold : .medium)) .font(.caption2.weight(isToday ? .bold : .medium))
.foregroundStyle(isToday ? .primary : .secondary) .foregroundStyle(isToday ? .primary : .secondary)
.textCase(.uppercase) .textCase(.uppercase)
@@ -653,9 +665,10 @@ struct DayCell: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 38, height: 38) .frame(width: 38, height: 38)
.foregroundColor(color) .foregroundColor(color)
.accessibilityLabel(mood.strValue)
Text(dateLabel) Text(dateLabel)
.font(.system(size: 13, weight: isToday ? .bold : .semibold)) .font(.caption.weight(isToday ? .bold : .semibold))
.foregroundStyle(isToday ? color : .secondary) .foregroundStyle(isToday ? color : .secondary)
} }
} }
@@ -679,26 +692,29 @@ struct LargeVotingView: View {
} }
var body: some View { var body: some View {
VStack(spacing: 24) { VStack(spacing: 16) {
Spacer() Spacer()
Text(hasSubscription ? promptText : "Subscribe to track your mood") Text(hasSubscription ? promptText : "Subscribe to track your mood")
.font(.title2.weight(.semibold)) .font(.title3.weight(.semibold))
.foregroundStyle(.primary) .foregroundStyle(.primary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.lineLimit(2) .lineLimit(2)
.minimumScaleFactor(0.8) .minimumScaleFactor(0.8)
.padding(.horizontal, 8)
// Large mood buttons in a row // Large mood buttons in a row - flexible spacing
HStack(spacing: 20) { HStack(spacing: 0) {
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
moodButton(for: mood) moodButton(for: mood)
.frame(maxWidth: .infinity)
} }
} }
Spacer() Spacer()
} }
.padding() .padding(.horizontal, 12)
.padding(.vertical, 16)
} }
@ViewBuilder @ViewBuilder
@@ -708,29 +724,35 @@ struct LargeVotingView: View {
moodButtonContent(for: mood) moodButtonContent(for: mood)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel(mood.strValue)
.accessibilityHint(String(localized: "Log this mood"))
} else { } else {
Link(destination: URL(string: "feels://subscribe")!) { Link(destination: URL(string: "feels://subscribe")!) {
moodButtonContent(for: mood) moodButtonContent(for: mood)
} }
.accessibilityLabel(mood.strValue)
.accessibilityHint(String(localized: "Open app to subscribe"))
} }
} }
private func moodButtonContent(for mood: Mood) -> some View { private func moodButtonContent(for mood: Mood) -> some View {
VStack(spacing: 8) { VStack(spacing: 4) {
moodImages.icon(forMood: mood) moodImages.icon(forMood: mood)
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 56, height: 56) .frame(width: 40, height: 40)
.foregroundColor(moodTint.color(forMood: mood)) .foregroundColor(moodTint.color(forMood: mood))
Text(mood.strValue) Text(mood.widgetDisplayName)
.font(.caption.weight(.medium)) .font(.caption2.weight(.medium))
.foregroundColor(moodTint.color(forMood: mood)) .foregroundColor(moodTint.color(forMood: mood))
.lineLimit(1)
.minimumScaleFactor(0.8)
} }
.padding(.vertical, 12) .padding(.vertical, 8)
.padding(.horizontal, 8) .padding(.horizontal, 4)
.background( .background(
RoundedRectangle(cornerRadius: 16) RoundedRectangle(cornerRadius: 12)
.fill(moodTint.color(forMood: mood).opacity(0.15)) .fill(moodTint.color(forMood: mood).opacity(0.15))
) )
} }
@@ -899,10 +921,14 @@ struct InlineVotingView: View {
moodIcon(for: mood) moodIcon(for: mood)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel(mood.strValue)
.accessibilityHint(String(localized: "Log this mood"))
} else { } else {
Link(destination: URL(string: "feels://subscribe")!) { Link(destination: URL(string: "feels://subscribe")!) {
moodIcon(for: mood) moodIcon(for: mood)
} }
.accessibilityLabel(mood.strValue)
.accessibilityHint(String(localized: "Open app to subscribe"))
} }
} }
@@ -924,6 +950,7 @@ struct EntryCard: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50, alignment: .center) .frame(width: 50, height: 50, alignment: .center)
.foregroundColor(timeLineView.color) .foregroundColor(timeLineView.color)
.accessibilityLabel(timeLineView.mood.strValue)
} }
} }
@@ -1011,115 +1038,364 @@ struct FeelsGraphicWidget: Widget {
// MARK: - Preview Helpers // MARK: - Preview Helpers
private extension FeelsWidget_Previews { private enum WidgetPreviewHelpers {
static func sampleTimelineViews(count: Int) -> [WatchTimelineView] { static func sampleTimelineViews(count: Int, startMood: Mood = .great) -> [WatchTimelineView] {
let moods: [Mood] = [.great, .good, .average, .bad, .horrible] let moods: [Mood] = [.great, .good, .average, .bad, .horrible]
let startIndex = moods.firstIndex(of: startMood) ?? 0
return (0..<count).map { index in return (0..<count).map { index in
let mood = moods[index % moods.count] let mood = moods[(startIndex + index) % moods.count]
return WatchTimelineView( return WatchTimelineView(
image: EmojiMoodImages.icon(forMood: mood), image: EmojiMoodImages.icon(forMood: mood),
graphic: EmojiMoodImages.icon(forMood: mood), graphic: EmojiMoodImages.icon(forMood: mood),
date: Calendar.current.date(byAdding: .day, value: -index, to: Date())!, date: Calendar.current.date(byAdding: .day, value: -index, to: Date())!,
color: MoodTints.Default.color(forMood: mood), color: MoodTints.Default.color(forMood: mood),
secondaryColor: MoodTints.Default.secondary(forMood: mood) secondaryColor: MoodTints.Default.secondary(forMood: mood),
mood: mood
) )
} }
} }
static func sampleEntry(timelineCount: Int = 5) -> SimpleEntry { static func sampleEntry(timelineCount: Int = 5, hasVotedToday: Bool = true, hasSubscription: Bool = true, startMood: Mood = .great) -> SimpleEntry {
SimpleEntry( SimpleEntry(
date: Date(), date: Date(),
configuration: ConfigurationIntent(), configuration: ConfigurationIntent(),
timeLineViews: sampleTimelineViews(count: timelineCount), timeLineViews: sampleTimelineViews(count: timelineCount, startMood: startMood),
hasSubscription: true, hasSubscription: hasSubscription,
hasVotedToday: true hasVotedToday: hasVotedToday,
promptText: "How are you feeling today?"
) )
} }
} }
struct FeelsWidget_Previews: PreviewProvider { // MARK: - FeelsWidget Previews (Timeline Widget)
static var previews: some View {
Group {
// MARK: - FeelsWidget (Timeline)
FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 1))
.previewContext(WidgetPreviewContext(family: .systemSmall))
.previewDisplayName("Timeline - Small")
FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 5)) // Small - Logged States
.previewContext(WidgetPreviewContext(family: .systemMedium)) #Preview("Timeline Small - Great", as: .systemSmall) {
.previewDisplayName("Timeline - Medium") FeelsWidget()
} timeline: {
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .great)
}
FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 10)) #Preview("Timeline Small - Good", as: .systemSmall) {
.previewContext(WidgetPreviewContext(family: .systemLarge)) FeelsWidget()
.previewDisplayName("Timeline - Large") } timeline: {
WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .good)
}
// MARK: - FeelsGraphicWidget (Mood Graphic) #Preview("Timeline Small - Average", as: .systemSmall) {
FeelsGraphicWidgetEntryView(entry: sampleEntry(timelineCount: 2)) FeelsWidget()
.previewContext(WidgetPreviewContext(family: .systemSmall)) } timeline: {
.previewDisplayName("Mood Graphic - Small") WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .average)
}
// MARK: - FeelsIconWidget (Custom Icon) #Preview("Timeline Small - Bad", as: .systemSmall) {
FeelsIconWidgetEntryView(entry: sampleEntry()) FeelsWidget()
.previewContext(WidgetPreviewContext(family: .systemSmall)) } timeline: {
.previewDisplayName("Custom Icon - Small") WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .bad)
}
// MARK: - FeelsVoteWidget (Vote - Not Voted) #Preview("Timeline Small - Horrible", as: .systemSmall) {
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( FeelsWidget()
date: Date(), } timeline: {
hasSubscription: true, WidgetPreviewHelpers.sampleEntry(timelineCount: 1, startMood: .horrible)
hasVotedToday: false, }
todaysMood: nil,
stats: nil,
promptText: "How are you feeling?"
))
.previewContext(WidgetPreviewContext(family: .systemSmall))
.previewDisplayName("Vote - Small (Not Voted)")
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( // Small - Voting States
date: Date(), #Preview("Timeline Small - Voting", as: .systemSmall) {
hasSubscription: true, FeelsWidget()
hasVotedToday: false, } timeline: {
todaysMood: nil, WidgetPreviewHelpers.sampleEntry(timelineCount: 1, hasVotedToday: false)
stats: nil, }
promptText: "How are you feeling?"
))
.previewContext(WidgetPreviewContext(family: .systemMedium))
.previewDisplayName("Vote - Medium (Not Voted)")
// MARK: - FeelsVoteWidget (Vote - Already Voted) #Preview("Timeline Small - Non-Subscriber", as: .systemSmall) {
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( FeelsWidget()
date: Date(), } timeline: {
hasSubscription: true, WidgetPreviewHelpers.sampleEntry(timelineCount: 1, hasVotedToday: false, hasSubscription: false)
hasVotedToday: true, }
todaysMood: .great,
stats: MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]),
promptText: ""
))
.previewContext(WidgetPreviewContext(family: .systemSmall))
.previewDisplayName("Vote - Small (Voted)")
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( // Medium - Logged States
date: Date(), #Preview("Timeline Medium - Logged", as: .systemMedium) {
hasSubscription: true, FeelsWidget()
hasVotedToday: true, } timeline: {
todaysMood: .good, WidgetPreviewHelpers.sampleEntry(timelineCount: 5)
stats: MoodStats(totalEntries: 45, moodCounts: [.great: 15, .good: 18, .average: 8, .bad: 3, .horrible: 1]), }
promptText: ""
))
.previewContext(WidgetPreviewContext(family: .systemMedium))
.previewDisplayName("Vote - Medium (Voted)")
// MARK: - FeelsVoteWidget (Non-Subscriber) // Medium - Voting States
FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( #Preview("Timeline Medium - Voting", as: .systemMedium) {
date: Date(), FeelsWidget()
hasSubscription: false, } timeline: {
hasVotedToday: false, WidgetPreviewHelpers.sampleEntry(timelineCount: 5, hasVotedToday: false)
todaysMood: nil, }
stats: nil,
promptText: "" #Preview("Timeline Medium - Non-Subscriber", as: .systemMedium) {
)) FeelsWidget()
.previewContext(WidgetPreviewContext(family: .systemSmall)) } timeline: {
.previewDisplayName("Vote - Small (Non-Subscriber)") WidgetPreviewHelpers.sampleEntry(timelineCount: 5, hasVotedToday: false, hasSubscription: false)
}
// Large - Logged States
#Preview("Timeline Large - Logged", as: .systemLarge) {
FeelsWidget()
} timeline: {
WidgetPreviewHelpers.sampleEntry(timelineCount: 10)
}
// Large - Voting States
#Preview("Timeline Large - Voting", as: .systemLarge) {
FeelsWidget()
} timeline: {
WidgetPreviewHelpers.sampleEntry(timelineCount: 10, hasVotedToday: false)
}
#Preview("Timeline Large - Non-Subscriber", as: .systemLarge) {
FeelsWidget()
} timeline: {
WidgetPreviewHelpers.sampleEntry(timelineCount: 10, hasVotedToday: false, hasSubscription: false)
}
// MARK: - FeelsGraphicWidget Previews (Mood Graphic)
#Preview("Graphic - Great", as: .systemSmall) {
FeelsGraphicWidget()
} timeline: {
WidgetPreviewHelpers.sampleEntry(timelineCount: 2, startMood: .great)
}
#Preview("Graphic - Good", as: .systemSmall) {
FeelsGraphicWidget()
} timeline: {
WidgetPreviewHelpers.sampleEntry(timelineCount: 2, startMood: .good)
}
#Preview("Graphic - Average", as: .systemSmall) {
FeelsGraphicWidget()
} timeline: {
WidgetPreviewHelpers.sampleEntry(timelineCount: 2, startMood: .average)
}
#Preview("Graphic - Bad", as: .systemSmall) {
FeelsGraphicWidget()
} timeline: {
WidgetPreviewHelpers.sampleEntry(timelineCount: 2, startMood: .bad)
}
#Preview("Graphic - Horrible", as: .systemSmall) {
FeelsGraphicWidget()
} timeline: {
WidgetPreviewHelpers.sampleEntry(timelineCount: 2, startMood: .horrible)
}
// MARK: - FeelsIconWidget Previews (Custom Icon)
#Preview("Custom Icon", as: .systemSmall) {
FeelsIconWidget()
} timeline: {
WidgetPreviewHelpers.sampleEntry()
}
// MARK: - Live Activity Previews (Lock Screen View)
#Preview("Live Activity - Not Logged") {
HStack(spacing: 16) {
VStack(spacing: 4) {
Image(systemName: "flame.fill")
.font(.title)
.foregroundColor(.orange)
Text("7")
.font(.title.bold())
Text("day streak")
.font(.caption)
.foregroundColor(.secondary)
}
Divider()
.frame(height: 50)
VStack(alignment: .leading) {
Text("Don't break your streak!")
.font(.headline)
Text("Tap to log your mood")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
}
.padding()
.background(Color(.systemBackground).opacity(0.8))
}
#Preview("Live Activity - Great") {
HStack(spacing: 16) {
VStack(spacing: 4) {
Image(systemName: "flame.fill")
.font(.title)
.foregroundColor(.orange)
Text("15")
.font(.title.bold())
Text("day streak")
.font(.caption)
.foregroundColor(.secondary)
}
Divider()
.frame(height: 50)
HStack(spacing: 8) {
Circle()
.fill(MoodTints.Default.color(forMood: .great))
.frame(width: 24, height: 24)
VStack(alignment: .leading) {
Text("Today's mood")
.font(.caption)
.foregroundColor(.secondary)
Text("Great")
.font(.headline)
} }
} }
Spacer()
}
.padding()
.background(Color(.systemBackground).opacity(0.8))
}
#Preview("Live Activity - Good") {
HStack(spacing: 16) {
VStack(spacing: 4) {
Image(systemName: "flame.fill")
.font(.title)
.foregroundColor(.orange)
Text("30")
.font(.title.bold())
Text("day streak")
.font(.caption)
.foregroundColor(.secondary)
}
Divider()
.frame(height: 50)
HStack(spacing: 8) {
Circle()
.fill(MoodTints.Default.color(forMood: .good))
.frame(width: 24, height: 24)
VStack(alignment: .leading) {
Text("Today's mood")
.font(.caption)
.foregroundColor(.secondary)
Text("Good")
.font(.headline)
}
}
Spacer()
}
.padding()
.background(Color(.systemBackground).opacity(0.8))
}
#Preview("Live Activity - Average") {
HStack(spacing: 16) {
VStack(spacing: 4) {
Image(systemName: "flame.fill")
.font(.title)
.foregroundColor(.orange)
Text("10")
.font(.title.bold())
Text("day streak")
.font(.caption)
.foregroundColor(.secondary)
}
Divider()
.frame(height: 50)
HStack(spacing: 8) {
Circle()
.fill(MoodTints.Default.color(forMood: .average))
.frame(width: 24, height: 24)
VStack(alignment: .leading) {
Text("Today's mood")
.font(.caption)
.foregroundColor(.secondary)
Text("Average")
.font(.headline)
}
}
Spacer()
}
.padding()
.background(Color(.systemBackground).opacity(0.8))
}
#Preview("Live Activity - Bad") {
HStack(spacing: 16) {
VStack(spacing: 4) {
Image(systemName: "flame.fill")
.font(.title)
.foregroundColor(.orange)
Text("5")
.font(.title.bold())
Text("day streak")
.font(.caption)
.foregroundColor(.secondary)
}
Divider()
.frame(height: 50)
HStack(spacing: 8) {
Circle()
.fill(MoodTints.Default.color(forMood: .bad))
.frame(width: 24, height: 24)
VStack(alignment: .leading) {
Text("Today's mood")
.font(.caption)
.foregroundColor(.secondary)
Text("Bad")
.font(.headline)
}
}
Spacer()
}
.padding()
.background(Color(.systemBackground).opacity(0.8))
}
#Preview("Live Activity - Horrible") {
HStack(spacing: 16) {
VStack(spacing: 4) {
Image(systemName: "flame.fill")
.font(.title)
.foregroundColor(.orange)
Text("3")
.font(.title.bold())
Text("day streak")
.font(.caption)
.foregroundColor(.secondary)
}
Divider()
.frame(height: 50)
HStack(spacing: 8) {
Circle()
.fill(MoodTints.Default.color(forMood: .horrible))
.frame(width: 24, height: 24)
VStack(alignment: .leading) {
Text("Today's mood")
.font(.caption)
.foregroundColor(.secondary)
Text("Horrible")
.font(.headline)
}
}
Spacer()
}
.padding()
.background(Color(.systemBackground).opacity(0.8))
} }

View File

@@ -75,7 +75,6 @@ enum Mood: Int {
var graphic: Image { var graphic: Image {
switch self { switch self {
case .horrible: case .horrible:
return Image("HorribleGraphic", bundle: .main) return Image("HorribleGraphic", bundle: .main)
case .bad: case .bad:

View File

@@ -25,6 +25,7 @@ struct OnboardingCustomizeOne: View {
.foregroundColor(Color(UIColor.darkText)) .foregroundColor(Color(UIColor.darkText))
.opacity(0.04) .opacity(0.04)
.scaleEffect(1.2, anchor: .trailing) .scaleEffect(1.2, anchor: .trailing)
.accessibilityHidden(true)
Spacer() Spacer()
} }

View File

@@ -25,6 +25,7 @@ struct OnboardingCustomizeTwo: View {
.foregroundColor(Color(UIColor.darkText)) .foregroundColor(Color(UIColor.darkText))
.opacity(0.04) .opacity(0.04)
.scaleEffect(1.2, anchor: .trailing) .scaleEffect(1.2, anchor: .trailing)
.accessibilityHidden(true)
Spacer() Spacer()
} }

View File

@@ -44,21 +44,21 @@ struct OnboardingDay: View {
.frame(width: 120, height: 120) .frame(width: 120, height: 120)
Image(systemName: "calendar") Image(systemName: "calendar")
.font(.system(size: 44)) .font(.largeTitle)
.foregroundColor(.white) .foregroundColor(.white)
} }
.padding(.bottom, 32) .padding(.bottom, 32)
// Title // Title
Text("Which day should\nyou rate?") Text("Which day should\nyou rate?")
.font(.system(size: 28, weight: .bold, design: .rounded)) .font(.title.weight(.bold))
.foregroundColor(.white) .foregroundColor(.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.bottom, 12) .padding(.bottom, 12)
// Subtitle // Subtitle
Text("When you get your reminder, do you want to rate today or yesterday?") Text("When you get your reminder, do you want to rate today or yesterday?")
.font(.system(size: 16, weight: .medium)) .font(.body.weight(.medium))
.foregroundColor(.white.opacity(0.85)) .foregroundColor(.white.opacity(0.85))
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal, 40) .padding(.horizontal, 40)
@@ -92,11 +92,11 @@ struct OnboardingDay: View {
// Tip // Tip
HStack(spacing: 12) { HStack(spacing: 12) {
Image(systemName: "lightbulb.fill") Image(systemName: "lightbulb.fill")
.font(.system(size: 18)) .font(.headline)
.foregroundColor(.yellow) .foregroundColor(.yellow)
Text("Tip: \"Yesterday\" works great for evening reminders") Text("Tip: \"Yesterday\" works great for evening reminders")
.font(.system(size: 14, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(.white.opacity(0.9)) .foregroundColor(.white.opacity(0.9))
} }
.padding(.horizontal, 30) .padding(.horizontal, 30)
@@ -124,7 +124,7 @@ struct DayOptionCard: View {
.frame(width: 46, height: 46) .frame(width: 46, height: 46)
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: 20)) .font(.title3)
.foregroundColor(isSelected ? Color(hex: "4facfe") : .white) .foregroundColor(isSelected ? Color(hex: "4facfe") : .white)
} }
.accessibilityHidden(true) .accessibilityHidden(true)
@@ -132,15 +132,15 @@ struct DayOptionCard: View {
// Text // Text
VStack(alignment: .leading, spacing: 3) { VStack(alignment: .leading, spacing: 3) {
Text(title) Text(title)
.font(.system(size: 17, weight: .semibold)) .font(.body.weight(.semibold))
.foregroundColor(isSelected ? Color(hex: "4facfe") : .white) .foregroundColor(isSelected ? Color(hex: "4facfe") : .white)
Text(subtitle) Text(subtitle)
.font(.system(size: 13)) .font(.caption)
.foregroundColor(isSelected ? Color(hex: "4facfe").opacity(0.8) : .white.opacity(0.8)) .foregroundColor(isSelected ? Color(hex: "4facfe").opacity(0.8) : .white.opacity(0.8))
Text(example) Text(example)
.font(.system(size: 11, weight: .medium)) .font(.caption2.weight(.medium))
.foregroundColor(isSelected ? Color(hex: "4facfe").opacity(0.6) : .white.opacity(0.6)) .foregroundColor(isSelected ? Color(hex: "4facfe").opacity(0.6) : .white.opacity(0.6))
.lineLimit(1) .lineLimit(1)
.minimumScaleFactor(0.8) .minimumScaleFactor(0.8)
@@ -151,7 +151,7 @@ struct DayOptionCard: View {
// Checkmark // Checkmark
if isSelected { if isSelected {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.font(.system(size: 22)) .font(.title3)
.foregroundColor(Color(hex: "4facfe")) .foregroundColor(Color(hex: "4facfe"))
.accessibilityHidden(true) .accessibilityHidden(true)
} }

View File

@@ -31,7 +31,7 @@ struct OnboardingStyle: View {
.frame(width: 100, height: 100) .frame(width: 100, height: 100)
Image(systemName: "paintpalette.fill") Image(systemName: "paintpalette.fill")
.font(.system(size: 40)) .font(.largeTitle)
.foregroundColor(.white) .foregroundColor(.white)
} }
.padding(.top, 40) .padding(.top, 40)
@@ -39,13 +39,13 @@ struct OnboardingStyle: View {
// Title // Title
Text("Make it yours") Text("Make it yours")
.font(.system(size: 28, weight: .bold, design: .rounded)) .font(.title.weight(.bold))
.foregroundColor(.white) .foregroundColor(.white)
.padding(.bottom, 8) .padding(.bottom, 8)
// Subtitle // Subtitle
Text("Choose your favorite style") Text("Choose your favorite style")
.font(.system(size: 16, weight: .medium)) .font(.body.weight(.medium))
.foregroundColor(.white.opacity(0.85)) .foregroundColor(.white.opacity(0.85))
.padding(.bottom, 20) .padding(.bottom, 20)
@@ -57,7 +57,7 @@ struct OnboardingStyle: View {
// Icon Style Section // Icon Style Section
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
Text("Icon Style") Text("Icon Style")
.font(.system(size: 14, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(.white.opacity(0.9)) .foregroundColor(.white.opacity(0.9))
.padding(.horizontal, 24) .padding(.horizontal, 24)
@@ -82,7 +82,7 @@ struct OnboardingStyle: View {
// Color Theme Section // Color Theme Section
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
Text("Mood Colors") Text("Mood Colors")
.font(.system(size: 14, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(.white.opacity(0.9)) .foregroundColor(.white.opacity(0.9))
.padding(.horizontal, 24) .padding(.horizontal, 24)
@@ -105,9 +105,9 @@ struct OnboardingStyle: View {
// Hint // Hint
HStack(spacing: 8) { HStack(spacing: 8) {
Image(systemName: "arrow.left.arrow.right") Image(systemName: "arrow.left.arrow.right")
.font(.system(size: 14)) .font(.subheadline)
Text("You can change these anytime in Customize") Text("You can change these anytime in Customize")
.font(.system(size: 13, weight: .medium)) .font(.caption.weight(.medium))
} }
.foregroundColor(.white.opacity(0.7)) .foregroundColor(.white.opacity(0.7))
.padding(.top, 20) .padding(.top, 20)
@@ -130,14 +130,15 @@ struct OnboardingStylePreview: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
.foregroundColor(moodTint.color(forMood: .good)) .foregroundColor(moodTint.color(forMood: .good))
.accessibilityLabel(Mood.good.strValue)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("Wednesday - 10th") Text("Wednesday - 10th")
.font(.system(size: 17, weight: .semibold)) .font(.body.weight(.semibold))
.foregroundColor(.white) .foregroundColor(.white)
Text(Mood.good.strValue) Text(Mood.good.strValue)
.font(.system(size: 14)) .font(.subheadline)
.foregroundColor(.white.opacity(0.8)) .foregroundColor(.white.opacity(0.8))
} }
@@ -167,6 +168,7 @@ struct OnboardingIconPackOption: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24) .frame(width: 24, height: 24)
.foregroundColor(moodTint.color(forMood: mood)) .foregroundColor(moodTint.color(forMood: mood))
.accessibilityLabel(mood.strValue)
} }
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)

View File

@@ -34,14 +34,14 @@ struct OnboardingSubscription: View {
.frame(width: 120, height: 120) .frame(width: 120, height: 120)
Image(systemName: "crown.fill") Image(systemName: "crown.fill")
.font(.system(size: 48)) .font(.largeTitle)
.foregroundColor(.yellow) .foregroundColor(.yellow)
} }
.padding(.bottom, 24) .padding(.bottom, 24)
// Title // Title
Text("Unlock the Full\nExperience") Text("Unlock the Full\nExperience")
.font(.system(size: 28, weight: .bold, design: .rounded)) .font(.title.weight(.bold))
.foregroundColor(.white) .foregroundColor(.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.bottom, 8) .padding(.bottom, 8)
@@ -101,10 +101,10 @@ struct OnboardingSubscription: View {
}) { }) {
HStack { HStack {
Image(systemName: "sparkles") Image(systemName: "sparkles")
.font(.system(size: 18, weight: .semibold)) .font(.headline.weight(.semibold))
Text("Get Personal Insights") Text("Get Personal Insights")
.font(.system(size: 18, weight: .bold)) .font(.headline.weight(.bold))
} }
.foregroundColor(Color(hex: "11998e")) .foregroundColor(Color(hex: "11998e"))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -125,7 +125,7 @@ struct OnboardingSubscription: View {
completionClosure(onboardingData) completionClosure(onboardingData)
}) { }) {
Text("Maybe Later") Text("Maybe Later")
.font(.system(size: 16, weight: .medium)) .font(.body.weight(.medium))
.foregroundColor(.white.opacity(0.8)) .foregroundColor(.white.opacity(0.8))
} }
.accessibilityLabel(String(localized: "Maybe Later")) .accessibilityLabel(String(localized: "Maybe Later"))
@@ -154,18 +154,18 @@ struct BenefitRow: View {
var body: some View { var body: some View {
HStack(spacing: 16) { HStack(spacing: 16) {
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: 22)) .font(.title3)
.foregroundColor(.white) .foregroundColor(.white)
.frame(width: 40) .frame(width: 40)
.accessibilityHidden(true) .accessibilityHidden(true)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(title) Text(title)
.font(.system(size: 16, weight: .semibold)) .font(.body.weight(.semibold))
.foregroundColor(.white) .foregroundColor(.white)
Text(description) Text(description)
.font(.system(size: 13)) .font(.caption)
.foregroundColor(.white.opacity(0.8)) .foregroundColor(.white.opacity(0.8))
} }

View File

@@ -36,21 +36,21 @@ struct OnboardingTime: View {
.frame(width: 120, height: 120) .frame(width: 120, height: 120)
Image(systemName: "bell.fill") Image(systemName: "bell.fill")
.font(.system(size: 44)) .font(.largeTitle)
.foregroundColor(.white) .foregroundColor(.white)
} }
.padding(.bottom, 32) .padding(.bottom, 32)
// Title // Title
Text("When should we\nremind you?") Text("When should we\nremind you?")
.font(.system(size: 28, weight: .bold, design: .rounded)) .font(.title.weight(.bold))
.foregroundColor(.white) .foregroundColor(.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.bottom, 12) .padding(.bottom, 12)
// Subtitle // Subtitle
Text("Pick a time that works for your daily check-in") Text("Pick a time that works for your daily check-in")
.font(.system(size: 16, weight: .medium)) .font(.body.weight(.medium))
.foregroundColor(.white.opacity(0.85)) .foregroundColor(.white.opacity(0.85))
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal, 40) .padding(.horizontal, 40)
@@ -80,12 +80,12 @@ struct OnboardingTime: View {
// Info text // Info text
HStack(spacing: 12) { HStack(spacing: 12) {
Image(systemName: "info.circle.fill") Image(systemName: "info.circle.fill")
.font(.system(size: 20)) .font(.title3)
.foregroundColor(.white.opacity(0.8)) .foregroundColor(.white.opacity(0.8))
.accessibilityHidden(true) .accessibilityHidden(true)
Text("You'll get a gentle reminder at \(formatter.string(from: onboardingData.date)) every day") Text("You'll get a gentle reminder at \(formatter.string(from: onboardingData.date)) every day")
.font(.system(size: 14, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(.white.opacity(0.9)) .foregroundColor(.white.opacity(0.9))
} }
.padding(.horizontal, 30) .padding(.horizontal, 30)

View File

@@ -23,6 +23,7 @@ struct OnboardingTitle: View {
.opacity(0.04) .opacity(0.04)
.scaleEffect(1.2) .scaleEffect(1.2)
.padding(.bottom, 55) .padding(.bottom, 55)
.accessibilityHidden(true)
ScrollView { ScrollView {
VStack{ VStack{
@@ -37,8 +38,7 @@ struct OnboardingTitle: View {
// onboardingData.title = option // onboardingData.title = option
}, label: { }, label: {
Text(option) Text(option)
.font(.system(size: 15)) .font(.subheadline.weight(.bold))
.fontWeight(.bold)
.foregroundColor(.white) .foregroundColor(.white)
.padding(10) .padding(10)
.background(RoundedRectangle(cornerRadius: 10).stroke().foregroundColor(Color.white)) .background(RoundedRectangle(cornerRadius: 10).stroke().foregroundColor(Color.white))

View File

@@ -32,20 +32,20 @@ struct OnboardingWelcome: View {
.frame(width: 120, height: 120) .frame(width: 120, height: 120)
Image(systemName: "heart.fill") Image(systemName: "heart.fill")
.font(.system(size: 50)) .font(.largeTitle)
.foregroundColor(.white) .foregroundColor(.white)
} }
.padding(.bottom, 40) .padding(.bottom, 40)
// Title // Title
Text("Welcome to Feels") Text("Welcome to Feels")
.font(.system(size: 34, weight: .bold, design: .rounded)) .font(.largeTitle.weight(.bold))
.foregroundColor(.white) .foregroundColor(.white)
.padding(.bottom, 12) .padding(.bottom, 12)
// Subtitle // Subtitle
Text("Track your mood, discover patterns,\nand understand yourself better.") Text("Track your mood, discover patterns,\nand understand yourself better.")
.font(.system(size: 18, weight: .medium)) .font(.headline.weight(.medium))
.foregroundColor(.white.opacity(0.9)) .foregroundColor(.white.opacity(0.9))
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal, 40) .padding(.horizontal, 40)
@@ -91,18 +91,18 @@ struct FeatureRow: View {
.frame(width: 50, height: 50) .frame(width: 50, height: 50)
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: 22)) .font(.title3)
.foregroundColor(.white) .foregroundColor(.white)
} }
.accessibilityHidden(true) .accessibilityHidden(true)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(title) Text(title)
.font(.system(size: 16, weight: .semibold)) .font(.body.weight(.semibold))
.foregroundColor(.white) .foregroundColor(.white)
Text(description) Text(description)
.font(.system(size: 14)) .font(.subheadline)
.foregroundColor(.white.opacity(0.8)) .foregroundColor(.white.opacity(0.8))
} }

View File

@@ -27,6 +27,7 @@ struct OnboardingWrapup: View {
.foregroundColor(Color(UIColor.darkText)) .foregroundColor(Color(UIColor.darkText))
.opacity(0.04) .opacity(0.04)
.scaleEffect(1.2, anchor: .trailing) .scaleEffect(1.2, anchor: .trailing)
.accessibilityHidden(true)
Spacer() Spacer()
} }

View File

@@ -90,11 +90,6 @@ extension Font {
static func scalable(_ style: Font.TextStyle, weight: Font.Weight = .regular) -> Font { static func scalable(_ style: Font.TextStyle, weight: Font.Weight = .regular) -> Font {
Font.system(style, design: .rounded).weight(weight) Font.system(style, design: .rounded).weight(weight)
} }
/// Returns a custom-sized font that scales with Dynamic Type
static func scaledSystem(size: CGFloat, weight: Font.Weight = .regular, design: Font.Design = .default, relativeTo style: Font.TextStyle = .body) -> Font {
Font.system(size: size, weight: weight, design: design)
}
} }
/// Property wrapper for scaled metrics that respect Dynamic Type /// Property wrapper for scaled metrics that respect Dynamic Type

View File

@@ -333,7 +333,7 @@ struct AuraVotingView: View {
// Label with elegant typography // Label with elegant typography
Text(mood.strValue) Text(mood.strValue)
.font(.system(size: 12, weight: .semibold, design: .rounded)) .font(.caption.weight(.semibold))
.foregroundColor(color) .foregroundColor(color)
.tracking(0.5) .tracking(0.5)
} }

View File

@@ -32,6 +32,7 @@ struct BGViewItem: View {
.foregroundColor(DefaultMoodTint.color(forMood: mood)) .foregroundColor(DefaultMoodTint.color(forMood: mood))
// .blur(radius: 3) // .blur(radius: 3)
.opacity(0.1) .opacity(0.1)
.accessibilityHidden(true)
} }
} }

View File

@@ -194,7 +194,7 @@ struct CustomizeView: View {
private var headerView: some View { private var headerView: some View {
HStack { HStack {
Text("Customize") Text("Customize")
.font(.system(size: 28, weight: .bold, design: .rounded)) .font(.title.weight(.bold))
.foregroundColor(textColor) .foregroundColor(textColor)
Spacer() Spacer()
@@ -214,7 +214,7 @@ struct SettingsSection<Content: View>: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Text(title.uppercased()) Text(title.uppercased())
.font(.system(size: 13, weight: .semibold)) .font(.caption.weight(.semibold))
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
.tracking(0.5) .tracking(0.5)
@@ -240,7 +240,7 @@ struct SettingsRow<Content: View>: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Text(title) Text(title)
.font(.system(size: 15, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(textColor.opacity(0.7)) .foregroundColor(textColor.opacity(0.7))
content content
@@ -272,7 +272,7 @@ struct ThemePickerCompact: View {
if theme == aTheme { if theme == aTheme {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.font(.system(size: 18)) .font(.headline)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.background(Circle().fill(.white).padding(2)) .background(Circle().fill(.white).padding(2))
.offset(x: 14, y: 14) .offset(x: 14, y: 14)
@@ -280,7 +280,7 @@ struct ThemePickerCompact: View {
} }
Text(aTheme.title) Text(aTheme.title)
.font(.system(size: 12, weight: .medium)) .font(.caption.weight(.medium))
.foregroundColor(theme == aTheme ? .accentColor : textColor.opacity(0.6)) .foregroundColor(theme == aTheme ? .accentColor : textColor.opacity(0.6))
} }
} }
@@ -310,7 +310,7 @@ struct TextColorPickerCompact: View {
.labelsHidden() .labelsHidden()
Text("Sample Text") Text("Sample Text")
.font(.system(size: 16, weight: .medium)) .font(.body.weight(.medium))
.foregroundColor(textColor) .foregroundColor(textColor)
Spacer() Spacer()
@@ -344,6 +344,7 @@ struct ImagePackPickerCompact: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 28, height: 28) .frame(width: 28, height: 28)
.foregroundColor(moodTint.color(forMood: mood)) .foregroundColor(moodTint.color(forMood: mood))
.accessibilityLabel(mood.strValue)
} }
} }
@@ -351,7 +352,7 @@ struct ImagePackPickerCompact: View {
if imagePack == images { if imagePack == images {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.font(.system(size: 22)) .font(.title2)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
} }
} }
@@ -398,7 +399,7 @@ struct TintPickerCompact: View {
if moodTint == tint { if moodTint == tint {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.font(.system(size: 22)) .font(.title2)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
} }
} }
@@ -432,11 +433,11 @@ struct TintPickerCompact: View {
if moodTint == .Custom { if moodTint == .Custom {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.font(.system(size: 22)) .font(.title2)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
} else { } else {
Text("Custom") Text("Custom")
.font(.system(size: 13)) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} }
@@ -485,9 +486,13 @@ struct VotingLayoutPickerCompact: View {
HStack(spacing: 10) { HStack(spacing: 10) {
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
Button(action: { Button(action: {
if UIAccessibility.isReduceMotionEnabled {
votingLayoutStyle = layout.rawValue
} else {
withAnimation(.easeInOut(duration: 0.2)) { withAnimation(.easeInOut(duration: 0.2)) {
votingLayoutStyle = layout.rawValue votingLayoutStyle = layout.rawValue
} }
}
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName]) EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
}) { }) {
VStack(spacing: 6) { VStack(spacing: 6) {
@@ -496,7 +501,7 @@ struct VotingLayoutPickerCompact: View {
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.4)) .foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.4))
Text(layout.displayName) Text(layout.displayName)
.font(.system(size: 11, weight: .medium)) .font(.caption2.weight(.medium))
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.5)) .foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.5))
} }
.frame(width: 70) .frame(width: 70)
@@ -608,7 +613,7 @@ struct CustomWidgetSection: View {
.frame(width: 60, height: 60) .frame(width: 60, height: 60)
Image(systemName: "plus") Image(systemName: "plus")
.font(.system(size: 24, weight: .medium)) .font(.title2.weight(.medium))
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} }
@@ -618,9 +623,9 @@ struct CustomWidgetSection: View {
Link(destination: URL(string: "https://support.apple.com/guide/iphone/add-widgets-iphb8f1bf206/ios")!) { Link(destination: URL(string: "https://support.apple.com/guide/iphone/add-widgets-iphb8f1bf206/ios")!) {
HStack(spacing: 6) { HStack(spacing: 6) {
Image(systemName: "questionmark.circle") Image(systemName: "questionmark.circle")
.font(.system(size: 14)) .font(.subheadline)
Text("How to add widgets") Text("How to add widgets")
.font(.system(size: 14)) .font(.subheadline)
} }
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
} }
@@ -659,12 +664,12 @@ struct PersonalityPackPickerCompact: View {
HStack { HStack {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(String(aPack.title())) Text(String(aPack.title()))
.font(.system(size: 15, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(textColor) .foregroundColor(textColor)
let strings = aPack.randomPushNotificationStrings() let strings = aPack.randomPushNotificationStrings()
Text(strings.body) Text(strings.body)
.font(.system(size: 13)) .font(.caption)
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
.lineLimit(2) .lineLimit(2)
} }
@@ -673,7 +678,7 @@ struct PersonalityPackPickerCompact: View {
if personalityPack == aPack { if personalityPack == aPack {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.font(.system(size: 22)) .font(.title2)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
} }
} }
@@ -736,7 +741,7 @@ struct DayFilterPickerCompact: View {
impactMed.impactOccurred() impactMed.impactOccurred()
}) { }) {
Text(day.prefix(2).uppercased()) Text(day.prefix(2).uppercased())
.font(.system(size: 13, weight: .semibold)) .font(.caption.weight(.semibold))
.foregroundColor(isActive ? .white : textColor.opacity(0.5)) .foregroundColor(isActive ? .white : textColor.opacity(0.5))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: 40) .frame(height: 40)
@@ -750,7 +755,7 @@ struct DayFilterPickerCompact: View {
} }
Text(String(localized: "day_picker_view_text")) Text(String(localized: "day_picker_view_text"))
.font(.system(size: 13)) .font(.caption)
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
@@ -774,15 +779,15 @@ struct SubscriptionBannerView: View {
private var subscribedView: some View { private var subscribedView: some View {
HStack(spacing: 12) { HStack(spacing: 12) {
Image(systemName: "checkmark.seal.fill") Image(systemName: "checkmark.seal.fill")
.font(.system(size: 28)) .font(.title)
.foregroundColor(.green) .foregroundColor(.green)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("Premium Active") Text("Premium Active")
.font(.system(size: 16, weight: .semibold)) .font(.body.weight(.semibold))
Text("You have full access") Text("You have full access")
.font(.system(size: 13)) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
@@ -793,7 +798,7 @@ struct SubscriptionBannerView: View {
await openSubscriptionManagement() await openSubscriptionManagement()
} }
} }
.font(.system(size: 14, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(.green) .foregroundColor(.green)
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 8) .padding(.vertical, 8)
@@ -813,23 +818,23 @@ struct SubscriptionBannerView: View {
}) { }) {
HStack(spacing: 12) { HStack(spacing: 12) {
Image(systemName: "crown.fill") Image(systemName: "crown.fill")
.font(.system(size: 28)) .font(.title)
.foregroundColor(.orange) .foregroundColor(.orange)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("Unlock Premium") Text("Unlock Premium")
.font(.system(size: 16, weight: .semibold)) .font(.body.weight(.semibold))
.foregroundColor(colorScheme == .dark ? .white : .black) .foregroundColor(colorScheme == .dark ? .white : .black)
Text("Month & Year views, Insights & more") Text("Month & Year views, Insights & more")
.font(.system(size: 13)) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
Spacer() Spacer()
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
.padding(16) .padding(16)
@@ -870,9 +875,13 @@ struct DayViewStylePickerCompact: View {
HStack(spacing: 10) { HStack(spacing: 10) {
ForEach(DayViewStyle.allCases, id: \.rawValue) { style in ForEach(DayViewStyle.allCases, id: \.rawValue) { style in
Button(action: { Button(action: {
if UIAccessibility.isReduceMotionEnabled {
dayViewStyle = style
} else {
withAnimation(.easeInOut(duration: 0.2)) { withAnimation(.easeInOut(duration: 0.2)) {
dayViewStyle = style dayViewStyle = style
} }
}
let impactMed = UIImpactFeedbackGenerator(style: .medium) let impactMed = UIImpactFeedbackGenerator(style: .medium)
impactMed.impactOccurred() impactMed.impactOccurred()
EventLogger.log(event: "change_day_view_style", withData: ["style": style.displayName]) EventLogger.log(event: "change_day_view_style", withData: ["style": style.displayName])
@@ -883,7 +892,7 @@ struct DayViewStylePickerCompact: View {
.foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.4)) .foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.4))
Text(style.displayName) Text(style.displayName)
.font(.system(size: 11, weight: .medium)) .font(.caption2.weight(.medium))
.foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.5)) .foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.5))
} }
.frame(width: 70) .frame(width: 70)
@@ -962,7 +971,7 @@ struct DayViewStylePickerCompact: View {
// Giant number with glowing orb // Giant number with glowing orb
HStack(spacing: 4) { HStack(spacing: 4) {
Text("17") Text("17")
.font(.system(size: 20, weight: .black, design: .rounded)) .font(.title3.weight(.black))
.foregroundStyle( .foregroundStyle(
LinearGradient(colors: [.green, .green.opacity(0.5)], startPoint: .top, endPoint: .bottom) LinearGradient(colors: [.green, .green.opacity(0.5)], startPoint: .top, endPoint: .bottom)
) )
@@ -983,7 +992,7 @@ struct DayViewStylePickerCompact: View {
Rectangle().frame(width: 34, height: 2) Rectangle().frame(width: 34, height: 2)
HStack(spacing: 4) { HStack(spacing: 4) {
Text("12") Text("12")
.font(.system(size: 18, weight: .regular, design: .serif)) .font(.headline.weight(.regular))
Rectangle().frame(width: 1, height: 20) Rectangle().frame(width: 1, height: 20)
VStack(alignment: .leading, spacing: 1) { VStack(alignment: .leading, spacing: 1) {
RoundedRectangle(cornerRadius: 1).frame(width: 12, height: 3) RoundedRectangle(cornerRadius: 1).frame(width: 12, height: 3)
@@ -1176,7 +1185,7 @@ struct DayViewStylePickerCompact: View {
.offset(x: -6, y: 4) .offset(x: -6, y: 4)
.blur(radius: 2) .blur(radius: 2)
Image(systemName: "gyroscope") Image(systemName: "gyroscope")
.font(.system(size: 12, weight: .medium)) .font(.caption.weight(.medium))
.foregroundColor(.white) .foregroundColor(.white)
} }
case .micro: case .micro:

View File

@@ -64,6 +64,8 @@ struct IconPickerView: View {
.frame(width: 50, height:50) .frame(width: 50, height:50)
.cornerRadius(10) .cornerRadius(10)
}) })
.accessibilityLabel(String(localized: "Default app icon"))
.accessibilityHint(String(localized: "Double tap to select"))
ForEach(iconSets, id: \.self.0){ iconSet in ForEach(iconSets, id: \.self.0){ iconSet in
@@ -78,6 +80,8 @@ struct IconPickerView: View {
.frame(width: 50, height:50) .frame(width: 50, height:50)
.cornerRadius(10) .cornerRadius(10)
}) })
.accessibilityLabel(String(localized: "App icon style \(iconSet.1.replacingOccurrences(of: "AppIcon", with: "").replacingOccurrences(of: "Image", with: ""))"))
.accessibilityHint(String(localized: "Double tap to select"))
} }
} }
.padding() .padding()

View File

@@ -31,6 +31,7 @@ struct ImagePackPickerView: View {
.foregroundColor( .foregroundColor(
moodTint.color(forMood: mood) moodTint.color(forMood: mood)
) )
.accessibilityLabel(mood.strValue)
} }
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
} }

View File

@@ -31,9 +31,13 @@ struct VotingLayoutPickerView: View {
HStack(spacing: 8) { HStack(spacing: 8) {
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
Button(action: { Button(action: {
if UIAccessibility.isReduceMotionEnabled {
votingLayoutStyle = layout.rawValue
} else {
withAnimation(.easeInOut(duration: 0.2)) { withAnimation(.easeInOut(duration: 0.2)) {
votingLayoutStyle = layout.rawValue votingLayoutStyle = layout.rawValue
} }
}
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName]) EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
}) { }) {
VStack(spacing: 6) { VStack(spacing: 6) {

View File

@@ -172,12 +172,12 @@ extension DayView {
HStack(spacing: 10) { HStack(spacing: 10) {
// Calendar icon // Calendar icon
Image(systemName: "calendar") Image(systemName: "calendar")
.font(.system(size: 16, weight: .semibold)) .font(.body.weight(.semibold))
.foregroundColor(textColor.opacity(0.6)) .foregroundColor(textColor.opacity(0.6))
.accessibilityHidden(true) .accessibilityHidden(true)
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))") Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
.font(.system(size: 20, weight: .bold, design: .rounded)) .font(.title3.weight(.bold))
.foregroundColor(textColor) .foregroundColor(textColor)
Spacer() Spacer()
@@ -194,7 +194,7 @@ extension DayView {
HStack(spacing: 0) { HStack(spacing: 0) {
// Large month number as hero element // Large month number as hero element
Text(String(format: "%02d", month)) Text(String(format: "%02d", month))
.font(.system(size: 48, weight: .black, design: .rounded)) .font(.largeTitle.weight(.black))
.foregroundStyle( .foregroundStyle(
LinearGradient( LinearGradient(
colors: [textColor, textColor.opacity(0.4)], colors: [textColor, textColor.opacity(0.4)],
@@ -206,12 +206,12 @@ extension DayView {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(Random.monthName(fromMonthInt: month).uppercased()) Text(Random.monthName(fromMonthInt: month).uppercased())
.font(.system(size: 14, weight: .bold, design: .rounded)) .font(.subheadline.weight(.bold))
.tracking(3) .tracking(3)
.foregroundColor(textColor) .foregroundColor(textColor)
Text(String(year)) Text(String(year))
.font(.system(size: 12, weight: .medium, design: .rounded)) .font(.caption.weight(.medium))
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
} }
@@ -253,12 +253,12 @@ extension DayView {
HStack(alignment: .firstTextBaseline, spacing: 12) { HStack(alignment: .firstTextBaseline, spacing: 12) {
// Large serif month name // Large serif month name
Text(Random.monthName(fromMonthInt: month).uppercased()) Text(Random.monthName(fromMonthInt: month).uppercased())
.font(.system(size: 28, weight: .regular, design: .serif)) .font(.title.weight(.regular))
.foregroundColor(textColor) .foregroundColor(textColor)
// Year in lighter weight // Year in lighter weight
Text(String(year)) Text(String(year))
.font(.system(size: 16, weight: .light, design: .serif)) .font(.body.weight(.light))
.italic() .italic()
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
@@ -266,7 +266,7 @@ extension DayView {
// Decorative flourish // Decorative flourish
Text("§") Text("§")
.font(.system(size: 20, weight: .regular, design: .serif)) .font(.title3.weight(.regular))
.foregroundColor(textColor.opacity(0.3)) .foregroundColor(textColor.opacity(0.3))
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
@@ -284,12 +284,12 @@ extension DayView {
HStack(spacing: 12) { HStack(spacing: 12) {
// Glowing terminal prompt // Glowing terminal prompt
Text(">") Text(">")
.font(.system(size: 18, weight: .bold, design: .monospaced)) .font(.headline.weight(.bold).monospaced())
.foregroundColor(Color(red: 0.4, green: 1.0, blue: 0.4)) .foregroundColor(Color(red: 0.4, green: 1.0, blue: 0.4))
.shadow(color: Color(red: 0.4, green: 1.0, blue: 0.4).opacity(0.8), radius: 4, x: 0, y: 0) .shadow(color: Color(red: 0.4, green: 1.0, blue: 0.4).opacity(0.8), radius: 4, x: 0, y: 0)
Text("\(Random.monthName(fromMonthInt: month).uppercased())_\(String(year))") Text("\(Random.monthName(fromMonthInt: month).uppercased())_\(String(year))")
.font(.system(size: 16, weight: .bold, design: .monospaced)) .font(.body.weight(.bold).monospaced())
.foregroundColor(.white) .foregroundColor(.white)
.shadow(color: .white.opacity(0.3), radius: 2, x: 0, y: 0) .shadow(color: .white.opacity(0.3), radius: 2, x: 0, y: 0)
@@ -329,12 +329,12 @@ extension DayView {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(Random.monthName(fromMonthInt: month)) Text(Random.monthName(fromMonthInt: month))
.font(.system(size: 18, weight: .thin)) .font(.headline.weight(.thin))
.tracking(4) .tracking(4)
.foregroundColor(textColor) .foregroundColor(textColor)
Text(String(year)) Text(String(year))
.font(.system(size: 11, weight: .ultraLight)) .font(.caption2.weight(.ultraLight))
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
} }
@@ -371,7 +371,7 @@ extension DayView {
// Glass content // Glass content
HStack(spacing: 12) { HStack(spacing: 12) {
Text(Random.monthName(fromMonthInt: month)) Text(Random.monthName(fromMonthInt: month))
.font(.system(size: 20, weight: .semibold)) .font(.title3.weight(.semibold))
.foregroundColor(textColor) .foregroundColor(textColor)
Capsule() Capsule()
@@ -379,7 +379,7 @@ extension DayView {
.frame(width: 4, height: 4) .frame(width: 4, height: 4)
Text(String(year)) Text(String(year))
.font(.system(size: 16, weight: .medium)) .font(.body.weight(.medium))
.foregroundColor(textColor.opacity(0.6)) .foregroundColor(textColor.opacity(0.6))
Spacer() Spacer()
@@ -405,11 +405,11 @@ extension DayView {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("SIDE A") Text("SIDE A")
.font(.system(size: 10, weight: .bold, design: .monospaced)) .font(.caption2.weight(.bold).monospaced())
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
Text("\(Random.monthName(fromMonthInt: month).uppercased()) '\(String(year).suffix(2))") Text("\(Random.monthName(fromMonthInt: month).uppercased()) '\(String(year).suffix(2))")
.font(.system(size: 16, weight: .black, design: .rounded)) .font(.body.weight(.black))
.foregroundColor(textColor) .foregroundColor(textColor)
.tracking(1) .tracking(1)
} }
@@ -418,7 +418,7 @@ extension DayView {
// Track counter // Track counter
Text(String(format: "%02d", month)) Text(String(format: "%02d", month))
.font(.system(size: 20, weight: .bold, design: .monospaced)) .font(.title3.weight(.bold).monospaced())
.foregroundColor(textColor.opacity(0.3)) .foregroundColor(textColor.opacity(0.3))
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
@@ -443,11 +443,11 @@ extension DayView {
HStack(spacing: 16) { HStack(spacing: 16) {
Text(Random.monthName(fromMonthInt: month)) Text(Random.monthName(fromMonthInt: month))
.font(.system(size: 22, weight: .light)) .font(.title2.weight(.light))
.foregroundColor(textColor) .foregroundColor(textColor)
Text(String(year)) Text(String(year))
.font(.system(size: 14, weight: .regular)) .font(.subheadline.weight(.regular))
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
Spacer() Spacer()
@@ -483,11 +483,11 @@ extension DayView {
.frame(width: 2) .frame(width: 2)
Text(Random.monthName(fromMonthInt: month)) Text(Random.monthName(fromMonthInt: month))
.font(.system(size: 18, weight: .regular, design: .serif)) .font(.headline.weight(.regular))
.foregroundColor(textColor) .foregroundColor(textColor)
Text(String(year)) Text(String(year))
.font(.system(size: 14, weight: .light, design: .serif)) .font(.subheadline.weight(.light))
.italic() .italic()
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
@@ -524,7 +524,7 @@ extension DayView {
return HStack(spacing: 0) { return HStack(spacing: 0) {
// Month number // Month number
Text(String(format: "%02d", month)) Text(String(format: "%02d", month))
.font(.system(size: 32, weight: .thin)) .font(.title.weight(.thin))
.foregroundColor(hasData ? barColor.opacity(0.6) : textColor.opacity(0.3)) .foregroundColor(hasData ? barColor.opacity(0.6) : textColor.opacity(0.3))
.frame(width: 50) .frame(width: 50)
@@ -554,16 +554,16 @@ extension DayView {
VStack(alignment: .trailing, spacing: 2) { VStack(alignment: .trailing, spacing: 2) {
Text(Random.monthName(fromMonthInt: month)) Text(Random.monthName(fromMonthInt: month))
.font(.system(size: 14, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(textColor) .foregroundColor(textColor)
if hasData { if hasData {
Text(String(format: "%.1f avg", averageMood + 1)) // Display as 1-5 Text(String(format: "%.1f avg", averageMood + 1)) // Display as 1-5
.font(.system(size: 10, weight: .semibold)) .font(.caption2.weight(.semibold))
.foregroundColor(barColor) .foregroundColor(barColor)
} else { } else {
Text(String(year)) Text(String(year))
.font(.system(size: 11, weight: .regular)) .font(.caption2.weight(.regular))
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
} }
} }
@@ -576,11 +576,11 @@ extension DayView {
private func patternSectionHeader(month: Int, year: Int) -> some View { private func patternSectionHeader(month: Int, year: Int) -> some View {
HStack(spacing: 10) { HStack(spacing: 10) {
Image(systemName: "calendar") Image(systemName: "calendar")
.font(.system(size: 16, weight: .semibold)) .font(.body.weight(.semibold))
.foregroundColor(textColor.opacity(0.6)) .foregroundColor(textColor.opacity(0.6))
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))") Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
.font(.system(size: 20, weight: .bold, design: .rounded)) .font(.title3.weight(.bold))
.foregroundColor(textColor) .foregroundColor(textColor)
Spacer() Spacer()
@@ -630,12 +630,12 @@ extension DayView {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(Random.monthName(fromMonthInt: month).uppercased()) Text(Random.monthName(fromMonthInt: month).uppercased())
.font(.system(size: 16, weight: .bold, design: .serif)) .font(.body.weight(.bold))
.foregroundColor(Color(red: 0.9, green: 0.85, blue: 0.75)) .foregroundColor(Color(red: 0.9, green: 0.85, blue: 0.75))
.shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1) .shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1)
Text(String(year)) Text(String(year))
.font(.system(size: 12, weight: .medium, design: .serif)) .font(.caption.weight(.medium))
.foregroundColor(Color(red: 0.8, green: 0.7, blue: 0.55)) .foregroundColor(Color(red: 0.8, green: 0.7, blue: 0.55))
} }
.padding(.leading, 12) .padding(.leading, 12)
@@ -726,17 +726,17 @@ extension DayView {
.blur(radius: 4) .blur(radius: 4)
Text(String(format: "%02d", month)) Text(String(format: "%02d", month))
.font(.system(size: 14, weight: .semibold, design: .rounded)) .font(.subheadline.weight(.semibold))
.foregroundColor(textColor) .foregroundColor(textColor)
} }
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(Random.monthName(fromMonthInt: month)) Text(Random.monthName(fromMonthInt: month))
.font(.system(size: 18, weight: .medium)) .font(.headline.weight(.medium))
.foregroundColor(textColor) .foregroundColor(textColor)
Text(String(year)) Text(String(year))
.font(.system(size: 12, weight: .regular)) .font(.caption.weight(.regular))
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
} }
@@ -811,18 +811,18 @@ extension DayView {
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
Image(systemName: "gyroscope") Image(systemName: "gyroscope")
.font(.system(size: 22, weight: .medium)) .font(.title2.weight(.medium))
.foregroundColor(.white) .foregroundColor(.white)
} }
.shadow(color: Color.purple.opacity(0.3), radius: 8, x: 0, y: 4) .shadow(color: Color.purple.opacity(0.3), radius: 8, x: 0, y: 4)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(Random.monthName(fromMonthInt: month)) Text(Random.monthName(fromMonthInt: month))
.font(.system(size: 20, weight: .semibold)) .font(.title3.weight(.semibold))
.foregroundColor(textColor) .foregroundColor(textColor)
Text(String(year)) Text(String(year))
.font(.system(size: 13, weight: .medium)) .font(.caption.weight(.medium))
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
} }
@@ -830,7 +830,7 @@ extension DayView {
// Tilt indicator // Tilt indicator
Image(systemName: "iphone.gen3.radiowaves.left.and.right") Image(systemName: "iphone.gen3.radiowaves.left.and.right")
.font(.system(size: 18)) .font(.headline)
.foregroundColor(textColor.opacity(0.3)) .foregroundColor(textColor.opacity(0.3))
} }
.padding(.horizontal, 18) .padding(.horizontal, 18)
@@ -852,15 +852,15 @@ extension DayView {
.frame(width: 3, height: 16) .frame(width: 3, height: 16)
Text("\(Random.monthName(fromMonthInt: month).prefix(3).uppercased())") Text("\(Random.monthName(fromMonthInt: month).prefix(3).uppercased())")
.font(.system(size: 11, weight: .bold, design: .monospaced)) .font(.caption2.weight(.bold).monospaced())
.foregroundColor(textColor.opacity(0.6)) .foregroundColor(textColor.opacity(0.6))
Text("") Text("")
.font(.system(size: 8)) .font(.caption2)
.foregroundColor(textColor.opacity(0.3)) .foregroundColor(textColor.opacity(0.3))
Text(String(year)) Text(String(year))
.font(.system(size: 11, weight: .medium, design: .monospaced)) .font(.caption2.weight(.medium).monospaced())
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
// Thin separator line // Thin separator line

View File

@@ -108,30 +108,31 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 32, height: 32) .frame(width: 32, height: 32)
.foregroundColor(isMissing ? .gray : .white) .foregroundColor(isMissing ? .gray : .white)
.accessibilityLabel(entry.mood.strValue)
} }
.shadow(color: isMissing ? .clear : moodColor.opacity(0.4), radius: 8, x: 0, y: 4) .shadow(color: isMissing ? .clear : moodColor.opacity(0.4), radius: 8, x: 0, y: 4)
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 6) { HStack(spacing: 6) {
Text(Random.weekdayName(fromDate: entry.forDate)) Text(Random.weekdayName(fromDate: entry.forDate))
.font(.system(size: 17, weight: .semibold)) .font(.body.weight(.semibold))
.foregroundColor(textColor) .foregroundColor(textColor)
Text("") Text("")
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
Text(Random.dayFormat(fromDate: entry.forDate)) Text(Random.dayFormat(fromDate: entry.forDate))
.font(.system(size: 17, weight: .medium)) .font(.body.weight(.medium))
.foregroundColor(textColor.opacity(0.8)) .foregroundColor(textColor.opacity(0.8))
} }
if isMissing { if isMissing {
Text(String(localized: "mood_value_missing_tap_to_add")) Text(String(localized: "mood_value_missing_tap_to_add"))
.font(.system(size: 14, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(.gray) .foregroundColor(.gray)
} else { } else {
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 14, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(moodColor) .foregroundColor(moodColor)
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 4) .padding(.vertical, 4)
@@ -145,7 +146,7 @@ struct EntryListView: View {
Spacer() Spacer()
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(textColor.opacity(0.3)) .foregroundColor(textColor.opacity(0.3))
} }
.padding(.horizontal, 18) .padding(.horizontal, 18)
@@ -184,20 +185,21 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 22, height: 22) .frame(width: 22, height: 22)
.foregroundColor(isMissing ? .gray : moodColor) .foregroundColor(isMissing ? .gray : moodColor)
.accessibilityLabel(entry.mood.strValue)
) )
VStack(alignment: .leading, spacing: 3) { VStack(alignment: .leading, spacing: 3) {
Text(entry.forDate, format: .dateTime.weekday(.wide).day()) Text(entry.forDate, format: .dateTime.weekday(.wide).day())
.font(.system(size: 16, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(textColor) .foregroundColor(textColor)
if isMissing { if isMissing {
Text(String(localized: "mood_value_missing_tap_to_add")) Text(String(localized: "mood_value_missing_tap_to_add"))
.font(.system(size: 13)) .font(.caption)
.foregroundColor(.gray) .foregroundColor(.gray)
} else { } else {
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 13, weight: .medium)) .font(.caption.weight(.medium))
.foregroundColor(moodColor) .foregroundColor(moodColor)
} }
} }
@@ -225,10 +227,10 @@ struct EntryListView: View {
// Date column // Date column
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text(entry.forDate, format: .dateTime.day()) Text(entry.forDate, format: .dateTime.day())
.font(.system(size: 20, weight: .bold, design: .rounded)) .font(.title3.weight(.bold))
.foregroundColor(textColor) .foregroundColor(textColor)
Text(entry.forDate, format: .dateTime.weekday(.abbreviated)) Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
.font(.system(size: 11, weight: .medium)) .font(.caption2.weight(.medium))
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
.textCase(.uppercase) .textCase(.uppercase)
} }
@@ -241,7 +243,7 @@ struct EntryListView: View {
.frame(height: 32) .frame(height: 32)
.overlay( .overlay(
Text(String(localized: "mood_value_missing_tap_to_add")) Text(String(localized: "mood_value_missing_tap_to_add"))
.font(.system(size: 12, weight: .medium)) .font(.caption.weight(.medium))
.foregroundColor(.gray) .foregroundColor(.gray)
) )
} else { } else {
@@ -255,9 +257,10 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16) .frame(width: 16, height: 16)
.foregroundColor(moodColor) .foregroundColor(moodColor)
.accessibilityLabel(entry.mood.strValue)
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 13, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(moodColor) .foregroundColor(moodColor)
} }
) )
@@ -280,19 +283,20 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 28, height: 28) .frame(width: 28, height: 28)
.foregroundColor(isMissing ? .gray : .white) .foregroundColor(isMissing ? .gray : .white)
.accessibilityLabel(entry.mood.strValue)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(entry.forDate, format: .dateTime.weekday(.wide).day()) Text(entry.forDate, format: .dateTime.weekday(.wide).day())
.font(.system(size: 15, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(isMissing ? textColor : .white) .foregroundColor(isMissing ? textColor : .white)
if isMissing { if isMissing {
Text(String(localized: "mood_value_missing_tap_to_add")) Text(String(localized: "mood_value_missing_tap_to_add"))
.font(.system(size: 13)) .font(.caption)
.foregroundColor(.gray) .foregroundColor(.gray)
} else { } else {
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 13, weight: .medium)) .font(.caption.weight(.medium))
.foregroundColor(.white.opacity(0.85)) .foregroundColor(.white.opacity(0.85))
} }
} }
@@ -300,7 +304,7 @@ struct EntryListView: View {
Spacer() Spacer()
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .semibold)) .font(.caption.weight(.semibold))
.foregroundColor(isMissing ? textColor.opacity(0.3) : .white.opacity(0.6)) .foregroundColor(isMissing ? textColor.opacity(0.3) : .white.opacity(0.6))
} }
.padding(.horizontal, 18) .padding(.horizontal, 18)
@@ -340,6 +344,7 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.padding(16) .padding(16)
.foregroundColor(isMissing ? .gray : .white) .foregroundColor(isMissing ? .gray : .white)
.accessibilityLabel(entry.mood.strValue)
} }
.shadow( .shadow(
color: isMissing ? .clear : moodColor.opacity(0.3), color: isMissing ? .clear : moodColor.opacity(0.3),
@@ -350,12 +355,12 @@ struct EntryListView: View {
// Day number // Day number
Text(entry.forDate, format: .dateTime.day()) Text(entry.forDate, format: .dateTime.day())
.font(.system(size: 16, weight: .bold, design: .rounded)) .font(.subheadline.weight(.bold))
.foregroundColor(textColor) .foregroundColor(textColor)
// Weekday abbreviation // Weekday abbreviation
Text(entry.forDate, format: .dateTime.weekday(.abbreviated)) Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
.font(.system(size: 11, weight: .medium)) .font(.caption2.weight(.medium))
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
.textCase(.uppercase) .textCase(.uppercase)
} }
@@ -372,7 +377,7 @@ struct EntryListView: View {
HStack(spacing: 0) { HStack(spacing: 0) {
// Giant day number - the visual hero // Giant day number - the visual hero
Text(entry.forDate, format: .dateTime.day()) Text(entry.forDate, format: .dateTime.day())
.font(.system(size: 64, weight: .black, design: .rounded)) .font(.largeTitle.weight(.black))
.foregroundStyle( .foregroundStyle(
isMissing isMissing
? LinearGradient(colors: [Color.gray.opacity(0.3), Color.gray.opacity(0.15)], startPoint: .top, endPoint: .bottom) ? LinearGradient(colors: [Color.gray.opacity(0.3), Color.gray.opacity(0.15)], startPoint: .top, endPoint: .bottom)
@@ -385,7 +390,7 @@ struct EntryListView: View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
// Weekday with elegant typography // Weekday with elegant typography
Text(entry.forDate, format: .dateTime.weekday(.wide)) Text(entry.forDate, format: .dateTime.weekday(.wide))
.font(.system(size: 13, weight: .semibold, design: .rounded)) .font(.caption.weight(.semibold))
.textCase(.uppercase) .textCase(.uppercase)
.tracking(2) .tracking(2)
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
@@ -423,21 +428,22 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.foregroundColor(isMissing ? .gray : .white) .foregroundColor(isMissing ? .gray : .white)
.accessibilityLabel(entry.mood.strValue)
} }
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
if isMissing { if isMissing {
Text(String(localized: "mood_value_missing_tap_to_add")) Text(String(localized: "mood_value_missing_tap_to_add"))
.font(.system(size: 15, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(.gray) .foregroundColor(.gray)
} else { } else {
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 20, weight: .bold, design: .rounded)) .font(.title3.weight(.bold))
.foregroundColor(textColor) .foregroundColor(textColor)
// Month context // Month context
Text(entry.forDate, format: .dateTime.month(.wide)) Text(entry.forDate, format: .dateTime.month(.wide))
.font(.system(size: 12, weight: .medium)) .font(.caption.weight(.medium))
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
} }
} }
@@ -510,12 +516,12 @@ struct EntryListView: View {
// Left column: Giant day number in serif // Left column: Giant day number in serif
VStack(alignment: .trailing, spacing: 0) { VStack(alignment: .trailing, spacing: 0) {
Text(entry.forDate, format: .dateTime.day()) Text(entry.forDate, format: .dateTime.day())
.font(.system(size: 72, weight: .regular, design: .serif)) .font(.largeTitle.weight(.regular))
.foregroundColor(textColor) .foregroundColor(textColor)
.frame(width: 80) .frame(width: 80)
Text(entry.forDate, format: .dateTime.weekday(.abbreviated).month(.abbreviated)) Text(entry.forDate, format: .dateTime.weekday(.abbreviated).month(.abbreviated))
.font(.system(size: 11, weight: .regular, design: .serif)) .font(.caption2.weight(.regular))
.italic() .italic()
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
.textCase(.uppercase) .textCase(.uppercase)
@@ -530,12 +536,12 @@ struct EntryListView: View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
if isMissing { if isMissing {
Text("Entry Missing") Text("Entry Missing")
.font(.system(size: 24, weight: .regular, design: .serif)) .font(.title2.weight(.regular))
.italic() .italic()
.foregroundColor(.gray) .foregroundColor(.gray)
Text("Tap to record your mood for this day") Text("Tap to record your mood for this day")
.font(.system(size: 13, weight: .regular, design: .serif)) .font(.caption.weight(.regular))
.foregroundColor(.gray.opacity(0.7)) .foregroundColor(.gray.opacity(0.7))
} else { } else {
// Pull-quote style mood name // Pull-quote style mood name
@@ -545,7 +551,7 @@ struct EntryListView: View {
.frame(width: 4) .frame(width: 4)
Text("\"\(entry.moodString)\"") Text("\"\(entry.moodString)\"")
.font(.system(size: 28, weight: .regular, design: .serif)) .font(.title.weight(.regular))
.italic() .italic()
.foregroundColor(textColor) .foregroundColor(textColor)
} }
@@ -557,9 +563,10 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.foregroundColor(moodColor) .foregroundColor(moodColor)
.accessibilityLabel(entry.mood.strValue)
Text("Recorded mood entry") Text("Recorded mood entry")
.font(.system(size: 12, weight: .regular, design: .serif)) .font(.caption.weight(.regular))
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
.textCase(.uppercase) .textCase(.uppercase)
.tracking(1.5) .tracking(1.5)
@@ -618,23 +625,24 @@ struct EntryListView: View {
.frame(width: 28, height: 28) .frame(width: 28, height: 28)
.foregroundColor(isMissing ? .gray : moodColor) .foregroundColor(isMissing ? .gray : moodColor)
.shadow(color: isMissing ? .clear : moodColor, radius: 8, x: 0, y: 0) .shadow(color: isMissing ? .clear : moodColor, radius: 8, x: 0, y: 0)
.accessibilityLabel(entry.mood.strValue)
} }
.frame(width: 52, height: 52) .frame(width: 52, height: 52)
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
// Date in monospace terminal style // Date in monospace terminal style
Text(entry.forDate, format: .dateTime.year().month(.twoDigits).day(.twoDigits)) Text(entry.forDate, format: .dateTime.year().month(.twoDigits).day(.twoDigits))
.font(.system(size: 13, weight: .medium, design: .monospaced)) .font(.caption.weight(.medium).monospaced())
.foregroundColor(Color(red: 0.4, green: 1.0, blue: 0.4)) // Terminal green .foregroundColor(Color(red: 0.4, green: 1.0, blue: 0.4)) // Terminal green
if isMissing { if isMissing {
Text("NO_DATA") Text("NO_DATA")
.font(.system(size: 18, weight: .bold, design: .monospaced)) .font(.headline.weight(.bold).monospaced())
.foregroundColor(.gray) .foregroundColor(.gray)
} else { } else {
// Mood in glowing text // Mood in glowing text
Text(entry.moodString.uppercased()) Text(entry.moodString.uppercased())
.font(.system(size: 18, weight: .black, design: .default)) .font(.headline.weight(.black))
.foregroundColor(moodColor) .foregroundColor(moodColor)
.shadow(color: moodColor.opacity(0.8), radius: 6, x: 0, y: 0) .shadow(color: moodColor.opacity(0.8), radius: 6, x: 0, y: 0)
.shadow(color: moodColor.opacity(0.4), radius: 12, x: 0, y: 0) .shadow(color: moodColor.opacity(0.4), radius: 12, x: 0, y: 0)
@@ -642,7 +650,7 @@ struct EntryListView: View {
// Weekday // Weekday
Text(entry.forDate, format: .dateTime.weekday(.wide)) Text(entry.forDate, format: .dateTime.weekday(.wide))
.font(.system(size: 11, weight: .medium, design: .monospaced)) .font(.caption2.weight(.medium).monospaced())
.foregroundColor(.white.opacity(0.4)) .foregroundColor(.white.opacity(0.4))
.textCase(.uppercase) .textCase(.uppercase)
} }
@@ -651,7 +659,7 @@ struct EntryListView: View {
// Chevron with glow // Chevron with glow
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .bold)) .font(.subheadline.weight(.bold))
.foregroundColor(isMissing ? .gray : moodColor) .foregroundColor(isMissing ? .gray : moodColor)
.shadow(color: isMissing ? .clear : moodColor, radius: 4, x: 0, y: 0) .shadow(color: isMissing ? .clear : moodColor, radius: 4, x: 0, y: 0)
} }
@@ -713,36 +721,37 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 18, height: 18) .frame(width: 18, height: 18)
.foregroundColor(isMissing ? .gray.opacity(0.5) : moodColor.opacity(0.8)) .foregroundColor(isMissing ? .gray.opacity(0.5) : moodColor.opacity(0.8))
.accessibilityLabel(entry.mood.strValue)
} }
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
// Day number with brush-like weight variation // Day number with brush-like weight variation
HStack(alignment: .firstTextBaseline, spacing: 4) { HStack(alignment: .firstTextBaseline, spacing: 4) {
Text(entry.forDate, format: .dateTime.day()) Text(entry.forDate, format: .dateTime.day())
.font(.system(size: 36, weight: .thin)) .font(.title.weight(.thin))
.foregroundColor(textColor) .foregroundColor(textColor)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(entry.forDate, format: .dateTime.month(.wide)) Text(entry.forDate, format: .dateTime.month(.wide))
.font(.system(size: 11, weight: .light)) .font(.caption2.weight(.light))
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
.textCase(.uppercase) .textCase(.uppercase)
.tracking(2) .tracking(2)
Text(entry.forDate, format: .dateTime.weekday(.wide)) Text(entry.forDate, format: .dateTime.weekday(.wide))
.font(.system(size: 11, weight: .light)) .font(.caption2.weight(.light))
.foregroundColor(textColor.opacity(0.35)) .foregroundColor(textColor.opacity(0.35))
} }
} }
if isMissing { if isMissing {
Text("") Text("")
.font(.system(size: 20, weight: .ultraLight)) .font(.title3.weight(.ultraLight))
.foregroundColor(.gray.opacity(0.4)) .foregroundColor(.gray.opacity(0.4))
} else { } else {
// Mood in delicate typography // Mood in delicate typography
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 17, weight: .light)) .font(.body.weight(.light))
.foregroundColor(moodColor) .foregroundColor(moodColor)
.tracking(1) .tracking(1)
} }
@@ -862,16 +871,17 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 26, height: 26) .frame(width: 26, height: 26)
.foregroundColor(isMissing ? .gray : .white) .foregroundColor(isMissing ? .gray : .white)
.accessibilityLabel(entry.mood.strValue)
} }
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text(entry.forDate, format: .dateTime.weekday(.wide)) Text(entry.forDate, format: .dateTime.weekday(.wide))
.font(.system(size: 15, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(textColor) .foregroundColor(textColor)
HStack(spacing: 8) { HStack(spacing: 8) {
Text(entry.forDate, format: .dateTime.month(.abbreviated).day()) Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
.font(.system(size: 13, weight: .medium)) .font(.caption.weight(.medium))
.foregroundColor(textColor.opacity(0.6)) .foregroundColor(textColor.opacity(0.6))
if !isMissing { if !isMissing {
@@ -880,11 +890,11 @@ struct EntryListView: View {
.frame(width: 4, height: 4) .frame(width: 4, height: 4)
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 13, weight: .semibold)) .font(.caption.weight(.semibold))
.foregroundColor(moodColor) .foregroundColor(moodColor)
} else { } else {
Text("Tap to add") Text("Tap to add")
.font(.system(size: 13)) .font(.caption)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
} }
@@ -894,7 +904,7 @@ struct EntryListView: View {
// Prismatic chevron // Prismatic chevron
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundStyle( .foregroundStyle(
isMissing isMissing
? AnyShapeStyle(Color.gray.opacity(0.3)) ? AnyShapeStyle(Color.gray.opacity(0.3))
@@ -919,7 +929,7 @@ struct EntryListView: View {
// Track number column // Track number column
VStack { VStack {
Text(entry.forDate, format: .dateTime.day()) Text(entry.forDate, format: .dateTime.day())
.font(.system(size: 24, weight: .bold, design: .monospaced)) .font(.title2.weight(.bold).monospaced())
.foregroundColor(isMissing ? .gray : moodColor) .foregroundColor(isMissing ? .gray : moodColor)
} }
.frame(width: 50) .frame(width: 50)
@@ -949,17 +959,17 @@ struct EntryListView: View {
// Track info // Track info
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(entry.forDate, format: .dateTime.weekday(.wide).month(.abbreviated)) Text(entry.forDate, format: .dateTime.weekday(.wide).month(.abbreviated))
.font(.system(size: 11, weight: .medium, design: .monospaced)) .font(.caption2.weight(.medium).monospaced())
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
.textCase(.uppercase) .textCase(.uppercase)
if isMissing { if isMissing {
Text("SIDE B - NO RECORDING") Text("SIDE B - NO RECORDING")
.font(.system(size: 14, weight: .bold, design: .rounded)) .font(.subheadline.weight(.bold))
.foregroundColor(.gray) .foregroundColor(.gray)
} else { } else {
Text(entry.moodString.uppercased()) Text(entry.moodString.uppercased())
.font(.system(size: 16, weight: .black, design: .rounded)) .font(.subheadline.weight(.black))
.foregroundColor(textColor) .foregroundColor(textColor)
.tracking(1) .tracking(1)
} }
@@ -992,6 +1002,7 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16) .frame(width: 16, height: 16)
.foregroundColor(isMissing ? .gray : moodColor) .foregroundColor(isMissing ? .gray : moodColor)
.accessibilityLabel(entry.mood.strValue)
} }
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
@@ -1068,21 +1079,22 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24) .frame(width: 24, height: 24)
.foregroundColor(.white) .foregroundColor(.white)
.accessibilityLabel(entry.mood.strValue)
} }
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
// Date with organic flow // Date with organic flow
HStack(spacing: 0) { HStack(spacing: 0) {
Text(entry.forDate, format: .dateTime.day()) Text(entry.forDate, format: .dateTime.day())
.font(.system(size: 32, weight: .light)) .font(.title.weight(.light))
.foregroundColor(textColor) .foregroundColor(textColor)
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text(entry.forDate, format: .dateTime.month(.abbreviated)) Text(entry.forDate, format: .dateTime.month(.abbreviated))
.font(.system(size: 12, weight: .medium)) .font(.caption.weight(.medium))
.foregroundColor(textColor.opacity(0.6)) .foregroundColor(textColor.opacity(0.6))
Text(entry.forDate, format: .dateTime.weekday(.abbreviated)) Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
.font(.system(size: 11, weight: .regular)) .font(.caption2.weight(.regular))
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
} }
.padding(.leading, 6) .padding(.leading, 6)
@@ -1090,11 +1102,11 @@ struct EntryListView: View {
if isMissing { if isMissing {
Text("No mood recorded") Text("No mood recorded")
.font(.system(size: 14, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(.gray) .foregroundColor(.gray)
} else { } else {
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 18, weight: .semibold)) .font(.headline.weight(.semibold))
.foregroundColor(moodColor) .foregroundColor(moodColor)
} }
} }
@@ -1139,11 +1151,11 @@ struct EntryListView: View {
// Handwritten-style date // Handwritten-style date
VStack(alignment: .center, spacing: 2) { VStack(alignment: .center, spacing: 2) {
Text(entry.forDate, format: .dateTime.day()) Text(entry.forDate, format: .dateTime.day())
.font(.system(size: 36, weight: .light, design: .serif)) .font(.title.weight(.light))
.foregroundColor(textColor) .foregroundColor(textColor)
Text(entry.forDate, format: .dateTime.weekday(.abbreviated)) Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
.font(.system(size: 12, weight: .medium, design: .serif)) .font(.caption.weight(.medium))
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
.textCase(.uppercase) .textCase(.uppercase)
} }
@@ -1158,12 +1170,12 @@ struct EntryListView: View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
// Lined paper effect // Lined paper effect
Text(entry.forDate, format: .dateTime.month(.wide).year()) Text(entry.forDate, format: .dateTime.month(.wide).year())
.font(.system(size: 12, weight: .regular, design: .serif)) .font(.caption.weight(.regular))
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
if isMissing { if isMissing {
Text("nothing written...") Text("nothing written...")
.font(.system(size: 18, weight: .regular, design: .serif)) .font(.headline.weight(.regular))
.italic() .italic()
.foregroundColor(.gray) .foregroundColor(.gray)
} else { } else {
@@ -1173,9 +1185,10 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24) .frame(width: 24, height: 24)
.foregroundColor(moodColor) .foregroundColor(moodColor)
.accessibilityLabel(entry.mood.strValue)
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 20, weight: .medium, design: .serif)) .font(.title3.weight(.medium))
.foregroundColor(textColor) .foregroundColor(textColor)
} }
} }
@@ -1209,11 +1222,11 @@ struct EntryListView: View {
// Date column - minimal // Date column - minimal
VStack(alignment: .trailing, spacing: 2) { VStack(alignment: .trailing, spacing: 2) {
Text(entry.forDate, format: .dateTime.day()) Text(entry.forDate, format: .dateTime.day())
.font(.system(size: 28, weight: .thin)) .font(.title.weight(.thin))
.foregroundColor(textColor) .foregroundColor(textColor)
Text(entry.forDate, format: .dateTime.weekday(.abbreviated)) Text(entry.forDate, format: .dateTime.weekday(.abbreviated))
.font(.system(size: 10, weight: .medium)) .font(.caption2.weight(.medium))
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
.textCase(.uppercase) .textCase(.uppercase)
} }
@@ -1263,15 +1276,16 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 28, height: 28) .frame(width: 28, height: 28)
.foregroundColor(isMissing ? .gray : .white) .foregroundColor(isMissing ? .gray : .white)
.accessibilityLabel(entry.mood.strValue)
.padding(.leading, 16) .padding(.leading, 16)
if isMissing { if isMissing {
Text("No entry") Text("No entry")
.font(.system(size: 15, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(.gray) .foregroundColor(.gray)
} else { } else {
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 17, weight: .semibold)) .font(.body.weight(.semibold))
.foregroundColor(.white) .foregroundColor(.white)
} }
@@ -1279,7 +1293,7 @@ struct EntryListView: View {
// Month indicator // Month indicator
Text(entry.forDate, format: .dateTime.month(.abbreviated)) Text(entry.forDate, format: .dateTime.month(.abbreviated))
.font(.system(size: 11, weight: .bold)) .font(.caption2.weight(.bold))
.foregroundColor(isMissing ? .gray : .white.opacity(0.7)) .foregroundColor(isMissing ? .gray : .white.opacity(0.7))
.padding(.trailing, 16) .padding(.trailing, 16)
} }
@@ -1309,20 +1323,21 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 28, height: 28) .frame(width: 28, height: 28)
.foregroundColor(.white) .foregroundColor(.white)
.accessibilityLabel(entry.mood.strValue)
} }
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text(entry.forDate, format: .dateTime.weekday(.wide).day()) Text(entry.forDate, format: .dateTime.weekday(.wide).day())
.font(.system(size: 17, weight: .semibold)) .font(.body.weight(.semibold))
.foregroundColor(textColor) .foregroundColor(textColor)
if isMissing { if isMissing {
Text("No mood recorded") Text("No mood recorded")
.font(.system(size: 14)) .font(.subheadline)
.foregroundColor(.gray) .foregroundColor(.gray)
} else { } else {
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 15, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(moodColor) .foregroundColor(moodColor)
} }
} }
@@ -1333,7 +1348,7 @@ struct EntryListView: View {
Spacer() Spacer()
Text(entry.forDate, format: .dateTime.month(.abbreviated)) Text(entry.forDate, format: .dateTime.month(.abbreviated))
.font(.system(size: 12, weight: .medium)) .font(.caption.weight(.medium))
.foregroundColor(textColor.opacity(0.6)) .foregroundColor(textColor.opacity(0.6))
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 6) .padding(.vertical, 6)
@@ -1362,6 +1377,7 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: iconSize, height: iconSize) .frame(width: iconSize, height: iconSize)
.foregroundColor(isMissing ? Color.gray.opacity(0.15) : moodColor.opacity(0.2)) .foregroundColor(isMissing ? Color.gray.opacity(0.15) : moodColor.opacity(0.2))
.accessibilityHidden(true)
.position( .position(
x: CGFloat(col) * spacing + (row.isMultiple(of: 2) ? spacing/2 : 0), x: CGFloat(col) * spacing + (row.isMultiple(of: 2) ? spacing/2 : 0),
y: CGFloat(row) * spacing y: CGFloat(row) * spacing
@@ -1467,21 +1483,22 @@ struct EntryListView: View {
.frame(width: 22, height: 22) .frame(width: 22, height: 22)
.foregroundColor(.white) .foregroundColor(.white)
.shadow(color: Color.black.opacity(0.3), radius: 1, x: 0, y: 1) .shadow(color: Color.black.opacity(0.3), radius: 1, x: 0, y: 1)
.accessibilityLabel(entry.mood.strValue)
} }
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(entry.forDate, format: .dateTime.weekday(.wide)) Text(entry.forDate, format: .dateTime.weekday(.wide))
.font(.system(size: 16, weight: .semibold, design: .serif)) .font(.subheadline.weight(.semibold))
.foregroundColor(Color(red: 0.95, green: 0.90, blue: 0.80)) .foregroundColor(Color(red: 0.95, green: 0.90, blue: 0.80))
.shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1) .shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1)
Text(entry.forDate, format: .dateTime.month(.abbreviated).day()) Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
.font(.system(size: 12, weight: .medium, design: .serif)) .font(.caption.weight(.medium))
.foregroundColor(Color(red: 0.8, green: 0.7, blue: 0.55)) .foregroundColor(Color(red: 0.8, green: 0.7, blue: 0.55))
if !isMissing { if !isMissing {
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 14, weight: .bold, design: .serif)) .font(.subheadline.weight(.bold))
.foregroundColor(Color(red: 0.95, green: 0.90, blue: 0.80)) .foregroundColor(Color(red: 0.95, green: 0.90, blue: 0.80))
.shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1) .shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1)
} }
@@ -1598,22 +1615,23 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24) .frame(width: 24, height: 24)
.foregroundColor(isMissing ? .gray : .white) .foregroundColor(isMissing ? .gray : .white)
.accessibilityLabel(entry.mood.strValue)
} }
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text(entry.forDate, format: .dateTime.weekday(.wide)) Text(entry.forDate, format: .dateTime.weekday(.wide))
.font(.system(size: 17, weight: .semibold)) .font(.body.weight(.semibold))
.foregroundColor(textColor) .foregroundColor(textColor)
HStack(spacing: 6) { HStack(spacing: 6) {
Text(entry.forDate, format: .dateTime.month(.abbreviated).day()) Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
.font(.system(size: 13)) .font(.caption)
.foregroundColor(textColor.opacity(0.6)) .foregroundColor(textColor.opacity(0.6))
if !isMissing { if !isMissing {
// Glass pill for mood // Glass pill for mood
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 12, weight: .medium)) .font(.caption.weight(.medium))
.foregroundColor(moodColor) .foregroundColor(moodColor)
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 4) .padding(.vertical, 4)
@@ -1633,7 +1651,7 @@ struct EntryListView: View {
// Glass chevron // Glass chevron
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
} }
.padding(18) .padding(18)
@@ -1699,15 +1717,16 @@ struct EntryListView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 18, height: 18) .frame(width: 18, height: 18)
.foregroundColor(isMissing ? .gray : moodColor) .foregroundColor(isMissing ? .gray : moodColor)
.accessibilityLabel(entry.mood.strValue)
// Date - very compact // Date - very compact
Text(entry.forDate, format: .dateTime.month(.abbreviated).day()) Text(entry.forDate, format: .dateTime.month(.abbreviated).day())
.font(.system(size: 13, weight: .medium, design: .monospaced)) .font(.caption.weight(.medium).monospaced())
.foregroundColor(textColor.opacity(0.7)) .foregroundColor(textColor.opacity(0.7))
// Weekday initial // Weekday initial
Text(String(Random.weekdayName(fromDate: entry.forDate).prefix(3))) Text(String(Random.weekdayName(fromDate: entry.forDate).prefix(3)))
.font(.system(size: 11, weight: .semibold)) .font(.caption2.weight(.semibold))
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
.frame(width: 28) .frame(width: 28)
@@ -1716,7 +1735,7 @@ struct EntryListView: View {
// Mood as tiny pill // Mood as tiny pill
if !isMissing { if !isMissing {
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 11, weight: .semibold)) .font(.caption2.weight(.semibold))
.foregroundColor(moodColor) .foregroundColor(moodColor)
.padding(.horizontal, 8) .padding(.horizontal, 8)
.padding(.vertical, 3) .padding(.vertical, 3)
@@ -1726,12 +1745,12 @@ struct EntryListView: View {
) )
} else { } else {
Text("tap") Text("tap")
.font(.system(size: 10, weight: .medium)) .font(.caption2.weight(.medium))
.foregroundColor(.gray.opacity(0.6)) .foregroundColor(.gray.opacity(0.6))
} }
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.font(.system(size: 10, weight: .semibold)) .font(.caption2.weight(.semibold))
.foregroundColor(textColor.opacity(0.25)) .foregroundColor(textColor.opacity(0.25))
} }
.padding(.horizontal, 14) .padding(.horizontal, 14)
@@ -1855,12 +1874,13 @@ struct MotionCardView: View {
x: -motionManager.xOffset * 0.3, x: -motionManager.xOffset * 0.3,
y: -motionManager.yOffset * 0.3 y: -motionManager.yOffset * 0.3
) )
.accessibilityLabel(entry.mood.strValue)
} }
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
// Day with motion // Day with motion
Text("\(dayNumber)") Text("\(dayNumber)")
.font(.system(size: 32, weight: .bold, design: .rounded)) .font(.title.weight(.bold))
.foregroundColor(isMissing ? .gray : moodColor) .foregroundColor(isMissing ? .gray : moodColor)
.offset( .offset(
x: motionManager.xOffset * 0.2, x: motionManager.xOffset * 0.2,
@@ -1869,7 +1889,7 @@ struct MotionCardView: View {
HStack(spacing: 6) { HStack(spacing: 6) {
Text(Random.weekdayName(fromDate: entry.forDate)) Text(Random.weekdayName(fromDate: entry.forDate))
.font(.system(size: 14, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(textColor.opacity(0.7)) .foregroundColor(textColor.opacity(0.7))
if !isMissing { if !isMissing {
@@ -1877,7 +1897,7 @@ struct MotionCardView: View {
.fill(moodColor) .fill(moodColor)
.frame(width: 4, height: 4) .frame(width: 4, height: 4)
Text(entry.moodString) Text(entry.moodString)
.font(.system(size: 14, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(moodColor) .foregroundColor(moodColor)
} }
} }
@@ -1886,7 +1906,7 @@ struct MotionCardView: View {
Spacer() Spacer()
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(textColor.opacity(0.3)) .foregroundColor(textColor.opacity(0.3))
.offset( .offset(
x: motionManager.xOffset * 0.5, x: motionManager.xOffset * 0.5,
@@ -1924,7 +1944,9 @@ class MotionManager: ObservableObject {
} }
private func startMotionUpdates() { private func startMotionUpdates() {
guard motionManager.isDeviceMotionAvailable else { return } // Respect Reduce Motion preference - skip parallax effect entirely
guard motionManager.isDeviceMotionAvailable,
!UIAccessibility.isReduceMotionEnabled else { return }
motionManager.deviceMotionUpdateInterval = 1/60 motionManager.deviceMotionUpdateInterval = 1/60
motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motion, error in motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motion, error in

View File

@@ -28,7 +28,7 @@ struct FeelsSubscriptionStoreView: View {
.frame(width: 100, height: 100) .frame(width: 100, height: 100)
Image(systemName: "heart.fill") Image(systemName: "heart.fill")
.font(.system(size: 44)) .font(.largeTitle)
.foregroundStyle( .foregroundStyle(
LinearGradient( LinearGradient(
colors: [.pink, .red], colors: [.pink, .red],
@@ -40,10 +40,10 @@ struct FeelsSubscriptionStoreView: View {
VStack(spacing: 8) { VStack(spacing: 8) {
Text("Unlock Premium") Text("Unlock Premium")
.font(.system(size: 28, weight: .bold, design: .rounded)) .font(.title.weight(.bold))
Text("Get unlimited access to all features") Text("Get unlimited access to all features")
.font(.system(size: 16)) .font(.body)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
@@ -80,11 +80,11 @@ struct FeatureHighlight: View {
var body: some View { var body: some View {
HStack(spacing: 12) { HStack(spacing: 12) {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.font(.system(size: 18)) .font(.headline)
.foregroundColor(.green) .foregroundColor(.green)
Text(text) Text(text)
.font(.system(size: 15, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(.primary) .foregroundColor(.primary)
Spacer() Spacer()

View File

@@ -26,7 +26,7 @@ struct InsightsView: View {
// Header // Header
HStack { HStack {
Text("Insights") Text("Insights")
.font(.system(size: 28, weight: .bold, design: .rounded)) .font(.title.weight(.bold))
.foregroundColor(textColor) .foregroundColor(textColor)
Spacer() Spacer()
@@ -34,9 +34,9 @@ struct InsightsView: View {
if viewModel.isAIAvailable { if viewModel.isAIAvailable {
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: "sparkles") Image(systemName: "sparkles")
.font(.system(size: 12, weight: .medium)) .font(.caption.weight(.medium))
Text("AI") Text("AI")
.font(.system(size: 12, weight: .semibold)) .font(.caption.weight(.semibold))
} }
.foregroundColor(.white) .foregroundColor(.white)
.padding(.horizontal, 8) .padding(.horizontal, 8)
@@ -118,7 +118,7 @@ struct InsightsView: View {
.frame(width: 100, height: 100) .frame(width: 100, height: 100)
Image(systemName: "sparkles") Image(systemName: "sparkles")
.font(.system(size: 44)) .font(.largeTitle)
.foregroundStyle( .foregroundStyle(
LinearGradient( LinearGradient(
colors: [.purple, .blue], colors: [.purple, .blue],
@@ -131,12 +131,12 @@ struct InsightsView: View {
// Text // Text
VStack(spacing: 12) { VStack(spacing: 12) {
Text("Unlock AI-Powered Insights") Text("Unlock AI-Powered Insights")
.font(.system(size: 24, weight: .bold, design: .rounded)) .font(.title2.weight(.bold))
.foregroundColor(textColor) .foregroundColor(textColor)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Text("Discover patterns in your mood, get personalized recommendations, and understand what affects how you feel.") Text("Discover patterns in your mood, get personalized recommendations, and understand what affects how you feel.")
.font(.system(size: 16)) .font(.body)
.foregroundColor(textColor.opacity(0.7)) .foregroundColor(textColor.opacity(0.7))
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal, 32) .padding(.horizontal, 32)
@@ -150,7 +150,7 @@ struct InsightsView: View {
Image(systemName: "sparkles") Image(systemName: "sparkles")
Text("Get Personal Insights") Text("Get Personal Insights")
} }
.font(.system(size: 18, weight: .bold)) .font(.headline.weight(.bold))
.foregroundColor(.white) .foregroundColor(.white)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 16) .padding(.vertical, 16)
@@ -202,14 +202,20 @@ struct InsightsSectionView: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Section Header // Section Header
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() } }) { Button(action: {
if UIAccessibility.isReduceMotionEnabled {
isExpanded.toggle()
} else {
withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() }
}
}) {
HStack { HStack {
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: 18, weight: .medium)) .font(.headline.weight(.medium))
.foregroundColor(textColor.opacity(0.6)) .foregroundColor(textColor.opacity(0.6))
Text(title) Text(title)
.font(.system(size: 20, weight: .bold)) .font(.title3.weight(.bold))
.foregroundColor(textColor) .foregroundColor(textColor)
// Loading indicator in header // Loading indicator in header
@@ -222,7 +228,7 @@ struct InsightsSectionView: View {
Spacer() Spacer()
Image(systemName: isExpanded ? "chevron.up" : "chevron.down") Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
.font(.system(size: 12, weight: .semibold)) .font(.caption.weight(.semibold))
.foregroundColor(textColor.opacity(0.4)) .foregroundColor(textColor.opacity(0.4))
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
@@ -277,6 +283,7 @@ struct InsightsSectionView: View {
removal: .opacity removal: .opacity
)) ))
.animation( .animation(
UIAccessibility.isReduceMotionEnabled ? nil :
.spring(response: 0.4, dampingFraction: 0.8) .spring(response: 0.4, dampingFraction: 0.8)
.delay(Double(index) * 0.05), .delay(Double(index) * 0.05),
value: insights.count value: insights.count
@@ -294,7 +301,7 @@ struct InsightsSectionView: View {
.fill(colorScheme == .dark ? Color(.systemGray6) : .white) .fill(colorScheme == .dark ? Color(.systemGray6) : .white)
) )
.padding(.horizontal) .padding(.horizontal)
.animation(.easeInOut(duration: 0.2), value: isExpanded) .animation(UIAccessibility.isReduceMotionEnabled ? nil : .easeInOut(duration: 0.2), value: isExpanded)
} }
} }
@@ -336,15 +343,18 @@ struct InsightSkeletonView: View {
) )
.opacity(isAnimating ? 0.6 : 1.0) .opacity(isAnimating ? 0.6 : 1.0)
.animation( .animation(
UIAccessibility.isReduceMotionEnabled ? nil :
.easeInOut(duration: 0.8) .easeInOut(duration: 0.8)
.repeatForever(autoreverses: true), .repeatForever(autoreverses: true),
value: isAnimating value: isAnimating
) )
.onAppear { .onAppear {
if !UIAccessibility.isReduceMotionEnabled {
isAnimating = true isAnimating = true
} }
} }
} }
}
// MARK: - Insight Card View // MARK: - Insight Card View
@@ -376,9 +386,10 @@ struct InsightCardView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 22, height: 22) .frame(width: 22, height: 22)
.foregroundColor(accentColor) .foregroundColor(accentColor)
.accessibilityLabel(mood.strValue)
} else { } else {
Image(systemName: insight.icon) Image(systemName: insight.icon)
.font(.system(size: 18, weight: .semibold)) .font(.headline.weight(.semibold))
.foregroundColor(accentColor) .foregroundColor(accentColor)
} }
} }
@@ -386,11 +397,11 @@ struct InsightCardView: View {
// Text Content // Text Content
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(insight.title) Text(insight.title)
.font(.system(size: 15, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(textColor) .foregroundColor(textColor)
Text(insight.description) Text(insight.description)
.font(.system(size: 14)) .font(.subheadline)
.foregroundColor(textColor.opacity(0.7)) .foregroundColor(textColor.opacity(0.7))
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} }

View File

@@ -31,7 +31,7 @@ struct LockScreenView: View {
// App icon / lock icon // App icon / lock icon
VStack(spacing: 20) { VStack(spacing: 20) {
Image(systemName: "lock.fill") Image(systemName: "lock.fill")
.font(.system(size: 60)) .font(.largeTitle)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Text("Feels is Locked") Text("Feels is Locked")

View File

@@ -216,13 +216,13 @@ struct MonthCard: View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Header with month/year // Header with month/year
Text("\(Random.monthName(fromMonthInt: month).uppercased()) \(String(year))") Text("\(Random.monthName(fromMonthInt: month).uppercased()) \(String(year))")
.font(.system(size: 32, weight: .heavy, design: .rounded)) .font(.title.weight(.heavy))
.foregroundColor(textColor) .foregroundColor(textColor)
.padding(.top, 40) .padding(.top, 40)
.padding(.bottom, 8) .padding(.bottom, 8)
Text("Monthly Mood Wrap") Text("Monthly Mood Wrap")
.font(.system(size: 16, weight: .medium, design: .rounded)) .font(.body.weight(.medium))
.foregroundColor(textColor.opacity(0.6)) .foregroundColor(textColor.opacity(0.6))
.padding(.bottom, 30) .padding(.bottom, 30)
@@ -238,15 +238,16 @@ struct MonthCard: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.foregroundColor(.white) .foregroundColor(.white)
.padding(24) .padding(24)
.accessibilityLabel(topMood.strValue)
) )
.shadow(color: moodTint.color(forMood: topMood).opacity(0.5), radius: 20, x: 0, y: 10) .shadow(color: moodTint.color(forMood: topMood).opacity(0.5), radius: 20, x: 0, y: 10)
Text("Top Mood") Text("Top Mood")
.font(.system(size: 14, weight: .medium, design: .rounded)) .font(.subheadline.weight(.medium))
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
Text(topMood.strValue.uppercased()) Text(topMood.strValue.uppercased())
.font(.system(size: 20, weight: .bold, design: .rounded)) .font(.title3.weight(.bold))
.foregroundColor(moodTint.color(forMood: topMood)) .foregroundColor(moodTint.color(forMood: topMood))
} }
.padding(.bottom, 30) .padding(.bottom, 30)
@@ -256,10 +257,10 @@ struct MonthCard: View {
HStack(spacing: 0) { HStack(spacing: 0) {
VStack(spacing: 4) { VStack(spacing: 4) {
Text("\(totalTrackedDays)") Text("\(totalTrackedDays)")
.font(.system(size: 36, weight: .bold, design: .rounded)) .font(.largeTitle.weight(.bold))
.foregroundColor(textColor) .foregroundColor(textColor)
Text("Days Tracked") Text("Days Tracked")
.font(.system(size: 12, weight: .medium, design: .rounded)) .font(.caption.weight(.medium))
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -279,6 +280,7 @@ struct MonthCard: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.foregroundColor(.white) .foregroundColor(.white)
.padding(7) .padding(7)
.accessibilityLabel(metric.mood.strValue)
) )
GeometryReader { geo in GeometryReader { geo in
@@ -294,7 +296,7 @@ struct MonthCard: View {
.frame(height: 12) .frame(height: 12)
Text("\(Int(metric.percent))%") Text("\(Int(metric.percent))%")
.font(.system(size: 14, weight: .semibold, design: .rounded)) .font(.subheadline.weight(.semibold))
.foregroundColor(textColor) .foregroundColor(textColor)
.frame(width: 40, alignment: .trailing) .frame(width: 40, alignment: .trailing)
} }
@@ -305,7 +307,7 @@ struct MonthCard: View {
// App branding // App branding
Text("ifeel") Text("ifeel")
.font(.system(size: 14, weight: .medium, design: .rounded)) .font(.subheadline.weight(.medium))
.foregroundColor(textColor.opacity(0.3)) .foregroundColor(textColor.opacity(0.3))
.padding(.bottom, 20) .padding(.bottom, 20)
} }
@@ -317,7 +319,13 @@ struct MonthCard: View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Month Header // Month Header
HStack { HStack {
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() } }) { Button(action: {
if UIAccessibility.isReduceMotionEnabled {
showStats.toggle()
} else {
withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() }
}
}) {
HStack { HStack {
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))") Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
.font(.title3.bold()) .font(.title3.bold())
@@ -453,6 +461,7 @@ struct MoodBarChart: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 18, height: 18) .frame(width: 18, height: 18)
.foregroundColor(moodTint.color(forMood: metric.mood)) .foregroundColor(moodTint.color(forMood: metric.mood))
.accessibilityLabel(metric.mood.strValue)
// Bar // Bar
GeometryReader { geo in GeometryReader { geo in
@@ -491,7 +500,7 @@ extension MonthView {
}, label: { }, label: {
Image(systemName: "gear") Image(systemName: "gear")
.foregroundColor(Color(UIColor.darkGray)) .foregroundColor(Color(UIColor.darkGray))
.font(.system(size: 20)) .font(.title3)
}).sheet(isPresented: $showingSheet) { }).sheet(isPresented: $showingSheet) {
SettingsView() SettingsView()
} }

View File

@@ -277,6 +277,7 @@ struct EntryDetailView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 34, height: 34) .frame(width: 34, height: 34)
.foregroundColor(.white) .foregroundColor(.white)
.accessibilityLabel(currentMood.strValue)
} }
.shadow(color: moodColor.opacity(0.4), radius: 8, x: 0, y: 4) .shadow(color: moodColor.opacity(0.4), radius: 8, x: 0, y: 4)
@@ -304,9 +305,13 @@ struct EntryDetailView: View {
ForEach(Mood.allValues) { mood in ForEach(Mood.allValues) { mood in
Button { Button {
// Update local state immediately for instant feedback // Update local state immediately for instant feedback
if UIAccessibility.isReduceMotionEnabled {
selectedMood = mood
} else {
withAnimation(.easeInOut(duration: 0.15)) { withAnimation(.easeInOut(duration: 0.15)) {
selectedMood = mood selectedMood = mood
} }
}
// Then persist the change // Then persist the change
onMoodUpdate(mood) onMoodUpdate(mood)
} label: { } label: {
@@ -320,6 +325,7 @@ struct EntryDetailView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 28, height: 28) .frame(width: 28, height: 28)
.foregroundColor(currentMood == mood ? .white : .gray) .foregroundColor(currentMood == mood ? .white : .gray)
.accessibilityLabel(mood.strValue)
) )
Text(mood.strValue) Text(mood.strValue)

View File

@@ -27,7 +27,7 @@ struct PhotoPickerView: View {
// Header // Header
VStack(spacing: 8) { VStack(spacing: 8) {
Image(systemName: "photo.on.rectangle.angled") Image(systemName: "photo.on.rectangle.angled")
.font(.system(size: 50)) .font(.largeTitle)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Text("Add a Photo") Text("Add a Photo")
@@ -281,7 +281,7 @@ struct PhotoGalleryView: View {
} else { } else {
VStack(spacing: 16) { VStack(spacing: 16) {
Image(systemName: "photo.badge.exclamationmark") Image(systemName: "photo.badge.exclamationmark")
.font(.system(size: 50)) .font(.largeTitle)
.foregroundColor(.gray) .foregroundColor(.gray)
Text("Photo not found") Text("Photo not found")

View File

@@ -28,7 +28,7 @@ struct SettingsTabView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Header // Header
Text("Settings") Text("Settings")
.font(.system(size: 28, weight: .bold, design: .rounded)) .font(.title.weight(.bold))
.foregroundColor(textColor) .foregroundColor(textColor)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16) .padding(.horizontal, 16)
@@ -92,14 +92,14 @@ struct UpgradeBannerView: View {
// Countdown timer // Countdown timer
HStack(spacing: 6) { HStack(spacing: 6) {
Image(systemName: "clock") Image(systemName: "clock")
.font(.system(size: 14, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(.orange) .foregroundColor(.orange)
if let expirationDate = trialExpirationDate { if let expirationDate = trialExpirationDate {
Text("\(Text("Trial expires in ").font(.system(size: 14, weight: .medium)).foregroundColor(textColor.opacity(0.8)))\(Text(expirationDate, style: .relative).font(.system(size: 14, weight: .bold)).foregroundColor(.orange))") Text("\(Text("Trial expires in ").font(.subheadline.weight(.medium)).foregroundColor(textColor.opacity(0.8)))\(Text(expirationDate, style: .relative).font(.subheadline.weight(.bold)).foregroundColor(.orange))")
} else { } else {
Text("Trial expired") Text("Trial expired")
.font(.system(size: 14, weight: .medium)) .font(.subheadline.weight(.medium))
.foregroundColor(.orange) .foregroundColor(.orange)
} }
} }
@@ -111,7 +111,7 @@ struct UpgradeBannerView: View {
showWhyUpgrade = true showWhyUpgrade = true
} label: { } label: {
Text("Why Upgrade?") Text("Why Upgrade?")
.font(.system(size: 14, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 10) .padding(.vertical, 10)
@@ -126,7 +126,7 @@ struct UpgradeBannerView: View {
showSubscriptionStore = true showSubscriptionStore = true
} label: { } label: {
Text("Subscribe") Text("Subscribe")
.font(.system(size: 14, weight: .semibold)) .font(.subheadline.weight(.semibold))
.foregroundColor(.white) .foregroundColor(.white)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 10) .padding(.vertical, 10)
@@ -157,7 +157,7 @@ struct WhyUpgradeView: View {
// Header // Header
VStack(spacing: 12) { VStack(spacing: 12) {
Image(systemName: "star.fill") Image(systemName: "star.fill")
.font(.system(size: 50)) .font(.largeTitle)
.foregroundStyle( .foregroundStyle(
LinearGradient( LinearGradient(
colors: [.orange, .pink], colors: [.orange, .pink],
@@ -167,7 +167,7 @@ struct WhyUpgradeView: View {
) )
Text("Unlock Premium") Text("Unlock Premium")
.font(.system(size: 28, weight: .bold)) .font(.title.weight(.bold))
Text("Get the most out of your mood tracking journey") Text("Get the most out of your mood tracking journey")
.font(.body) .font(.body)
@@ -262,7 +262,7 @@ struct PremiumBenefitRow: View {
var body: some View { var body: some View {
HStack(alignment: .top, spacing: 14) { HStack(alignment: .top, spacing: 14) {
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: 22)) .font(.title3)
.foregroundColor(iconColor) .foregroundColor(iconColor)
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
.background( .background(
@@ -272,10 +272,10 @@ struct PremiumBenefitRow: View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(title) Text(title)
.font(.system(size: 16, weight: .semibold)) .font(.body.weight(.semibold))
Text(description) Text(description)
.font(.system(size: 14)) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }

View File

@@ -174,13 +174,13 @@ struct YearCard: View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Header with year // Header with year
Text(String(year)) Text(String(year))
.font(.system(size: 48, weight: .heavy, design: .rounded)) .font(.largeTitle.weight(.heavy))
.foregroundColor(textColor) .foregroundColor(textColor)
.padding(.top, 40) .padding(.top, 40)
.padding(.bottom, 8) .padding(.bottom, 8)
Text("Year in Review") Text("Year in Review")
.font(.system(size: 18, weight: .medium, design: .rounded)) .font(.headline.weight(.medium))
.foregroundColor(textColor.opacity(0.6)) .foregroundColor(textColor.opacity(0.6))
.padding(.bottom, 30) .padding(.bottom, 30)
@@ -196,15 +196,16 @@ struct YearCard: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.foregroundColor(.white) .foregroundColor(.white)
.padding(28) .padding(28)
.accessibilityLabel(topMood.strValue)
) )
.shadow(color: moodTint.color(forMood: topMood).opacity(0.5), radius: 25, x: 0, y: 12) .shadow(color: moodTint.color(forMood: topMood).opacity(0.5), radius: 25, x: 0, y: 12)
Text("Top Mood") Text("Top Mood")
.font(.system(size: 14, weight: .medium, design: .rounded)) .font(.subheadline.weight(.medium))
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
Text(topMood.strValue.uppercased()) Text(topMood.strValue.uppercased())
.font(.system(size: 24, weight: .bold, design: .rounded)) .font(.title2.weight(.bold))
.foregroundColor(moodTint.color(forMood: topMood)) .foregroundColor(moodTint.color(forMood: topMood))
} }
.padding(.bottom, 30) .padding(.bottom, 30)
@@ -214,10 +215,10 @@ struct YearCard: View {
HStack(spacing: 0) { HStack(spacing: 0) {
VStack(spacing: 4) { VStack(spacing: 4) {
Text("\(totalEntries)") Text("\(totalEntries)")
.font(.system(size: 42, weight: .bold, design: .rounded)) .font(.largeTitle.weight(.bold))
.foregroundColor(textColor) .foregroundColor(textColor)
Text("Days Tracked") Text("Days Tracked")
.font(.system(size: 13, weight: .medium, design: .rounded)) .font(.caption.weight(.medium))
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -237,6 +238,7 @@ struct YearCard: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.foregroundColor(.white) .foregroundColor(.white)
.padding(8) .padding(8)
.accessibilityLabel(metric.mood.strValue)
) )
GeometryReader { geo in GeometryReader { geo in
@@ -252,7 +254,7 @@ struct YearCard: View {
.frame(height: 16) .frame(height: 16)
Text("\(Int(metric.percent))%") Text("\(Int(metric.percent))%")
.font(.system(size: 16, weight: .semibold, design: .rounded)) .font(.body.weight(.semibold))
.foregroundColor(textColor) .foregroundColor(textColor)
.frame(width: 45, alignment: .trailing) .frame(width: 45, alignment: .trailing)
} }
@@ -263,7 +265,7 @@ struct YearCard: View {
// App branding // App branding
Text("ifeel") Text("ifeel")
.font(.system(size: 14, weight: .medium, design: .rounded)) .font(.subheadline.weight(.medium))
.foregroundColor(textColor.opacity(0.3)) .foregroundColor(textColor.opacity(0.3))
.padding(.bottom, 20) .padding(.bottom, 20)
} }
@@ -275,7 +277,13 @@ struct YearCard: View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Year Header // Year Header
HStack { HStack {
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() } }) { Button(action: {
if UIAccessibility.isReduceMotionEnabled {
showStats.toggle()
} else {
withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() }
}
}) {
HStack { HStack {
Text(String(year)) Text(String(year))
.font(.title2.bold()) .font(.title2.bold())
@@ -324,6 +332,7 @@ struct YearCard: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16) .frame(width: 16, height: 16)
.foregroundColor(moodTint.color(forMood: metric.mood)) .foregroundColor(moodTint.color(forMood: metric.mood))
.accessibilityLabel(metric.mood.strValue)
GeometryReader { geo in GeometryReader { geo in
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
@@ -357,7 +366,7 @@ struct YearCard: View {
HStack(spacing: 2) { HStack(spacing: 2) {
ForEach(months.indices, id: \.self) { index in ForEach(months.indices, id: \.self) { index in
Text(months[index]) Text(months[index])
.font(.system(size: 9, weight: .medium)) .font(.caption2.weight(.medium))
.foregroundColor(textColor.opacity(0.5)) .foregroundColor(textColor.opacity(0.5))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }

View File

@@ -554,6 +554,7 @@
.feature-card:nth-child(4) .feature-icon { background: rgba(239, 190, 154, 0.2); } .feature-card:nth-child(4) .feature-icon { background: rgba(239, 190, 154, 0.2); }
.feature-card:nth-child(5) .feature-icon { background: rgba(165, 196, 212, 0.2); } .feature-card:nth-child(5) .feature-icon { background: rgba(165, 196, 212, 0.2); }
.feature-card:nth-child(6) .feature-icon { background: rgba(229, 168, 154, 0.2); } .feature-card:nth-child(6) .feature-icon { background: rgba(229, 168, 154, 0.2); }
.feature-card:nth-child(7) .feature-icon { background: rgba(94, 186, 175, 0.2); }
.feature-card h3 { .feature-card h3 {
font-size: 1.25rem; font-size: 1.25rem;
@@ -1191,6 +1192,12 @@
<h3>Private by Design</h3> <h3>Private by Design</h3>
<p>Your feelings are yours alone. All data stays on your devices with iCloud sync you control.</p> <p>Your feelings are yours alone. All data stays on your devices with iCloud sync you control.</p>
</div> </div>
<div class="feature-card reveal">
<div class="feature-icon"></div>
<h3>WCAG 2.1 AA Accessible</h3>
<p>Built for everyone. Full VoiceOver support, Dynamic Type, and high contrast ensure no one is left behind.</p>
</div>
</div> </div>
</section> </section>

View File

@@ -1107,6 +1107,12 @@
<h3>Privacy</h3> <h3>Privacy</h3>
<p>Your data stays on your devices. iCloud sync with no third-party access.</p> <p>Your data stays on your devices. iCloud sync with no third-party access.</p>
</div> </div>
<div class="feature-card fade-in">
<div class="feature-number">07</div>
<h3>WCAG 2.1 AA</h3>
<p>Built for everyone. Full VoiceOver support, Dynamic Type, and high contrast ensure no one is left behind.</p>
</div>
</div> </div>
</section> </section>

View File

@@ -491,6 +491,7 @@
.feature-icon.purple { background: rgba(168, 85, 247, 0.15); } .feature-icon.purple { background: rgba(168, 85, 247, 0.15); }
.feature-icon.red { background: rgba(248, 113, 113, 0.15); } .feature-icon.red { background: rgba(248, 113, 113, 0.15); }
.feature-icon.gray { background: rgba(148, 163, 184, 0.15); } .feature-icon.gray { background: rgba(148, 163, 184, 0.15); }
.feature-icon.cyan { background: rgba(34, 211, 238, 0.15); }
.feature-card h3 { .feature-card h3 {
font-size: 1.125rem; font-size: 1.125rem;
@@ -956,6 +957,11 @@
<h3>Privacy First</h3> <h3>Privacy First</h3>
<p>Your feelings stay yours. All data lives on your devices with iCloud sync.</p> <p>Your feelings stay yours. All data lives on your devices with iCloud sync.</p>
</div> </div>
<div class="feature-card reveal">
<div class="feature-icon cyan"></div>
<h3>WCAG 2.1 AA Accessible</h3>
<p>Built for everyone. Full VoiceOver support, Dynamic Type, and high contrast ensure no one is left behind.</p>
</div>
</div> </div>
</div> </div>
</section> </section>