Add comprehensive WCAG 2.1 AA accessibility support

- Add VoiceOver labels and hints to all voting layouts, settings, widgets,
  onboarding screens, and entry cells
- Add Reduce Motion support to button animations throughout the app
- Ensure 44x44pt minimum touch targets on widget mood buttons
- Enhance AccessibilityHelpers with Dynamic Type support, ScaledValue wrapper,
  and VoiceOver detection utilities
- Gate premium features (Insights, Month/Year views) behind subscription
- Update widgets to show subscription prompts for non-subscribers

🤖 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-23 23:26:21 -06:00
parent a6a6912183
commit 086f8b8807
24 changed files with 741 additions and 283 deletions

View File

@@ -158,28 +158,26 @@ struct FeelsVoteWidgetEntryView: View {
var body: some View {
Group {
if entry.hasSubscription {
if entry.hasVotedToday {
// Show stats after voting
VotedStatsView(entry: entry)
} else {
// Show voting buttons
VotingView(family: family, promptText: entry.promptText)
}
if entry.hasVotedToday {
// Already voted today - show stats (regardless of subscription status)
VotedStatsView(entry: entry)
} else {
// Non-subscriber view - tap to open app
NonSubscriberView()
// Not voted yet - show voting buttons
// If subscribed/in trial: buttons record votes
// If trial expired: buttons open app
VotingView(family: family, promptText: entry.promptText, hasSubscription: entry.hasSubscription)
}
}
.containerBackground(.fill.tertiary, for: .widget)
}
}
// MARK: - Voting View (for subscribers who haven't voted)
// MARK: - Voting View
struct VotingView: View {
let family: WidgetFamily
let promptText: String
let hasSubscription: Bool
private var moodTint: MoodTintable.Type {
UserDefaultsStore.moodTintable()
@@ -200,7 +198,7 @@ struct VotingView: View {
// MARK: - Small Widget: 3 over 2 grid
private var smallLayout: some View {
VStack(spacing: 0) {
Text(promptText)
Text(hasSubscription ? promptText : "Tap to open app")
.font(.caption)
.foregroundStyle(.primary)
.multilineTextAlignment(.center)
@@ -230,7 +228,7 @@ struct VotingView: View {
// MARK: - Medium Widget: Single row
private var mediumLayout: some View {
VStack {
Text(promptText)
Text(hasSubscription ? promptText : "Subscribe to track your mood")
.font(.headline)
.foregroundStyle(.primary)
.multilineTextAlignment(.center)
@@ -247,15 +245,37 @@ struct VotingView: View {
.padding()
}
@ViewBuilder
private func moodButton(for mood: Mood, size: CGFloat) -> some View {
Button(intent: VoteMoodIntent(mood: mood)) {
moodImages.icon(forMood: mood)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.foregroundColor(moodTint.color(forMood: mood))
// Ensure minimum 44x44 touch target for accessibility
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)
}
.buttonStyle(.plain)
.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)
}
.accessibilityLabel(mood.strValue)
.accessibilityHint(String(localized: "Open app to subscribe"))
}
.buttonStyle(.plain)
}
private func moodIcon(for mood: Mood, size: CGFloat) -> some View {
moodImages.icon(forMood: mood)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.foregroundColor(moodTint.color(forMood: mood))
}
}
@@ -311,6 +331,8 @@ struct VotedStatsView: View {
.foregroundStyle(.tertiary)
}
}
.accessibilityElement(children: .combine)
.accessibilityLabel(String(localized: "Mood logged: \(entry.todaysMood?.strValue ?? "")"))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(12)
@@ -407,6 +429,8 @@ struct NonSubscriberView: View {
.padding(12)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.accessibilityLabel(String(localized: "Track Your Mood"))
.accessibilityHint(String(localized: "Tap to open app and subscribe"))
}
}