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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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