Add 4 new mood icon styles and improve widget layouts

New mood icon styles:
- Weather (☀️⛈️)
- Garden (🌸🥀)
- Hearts (💖💔)
- Cosmic (🕳️)

Widget improvements:
- Small vote widget: 3-over-2 grid layout
- Medium vote widget: single horizontal row
- Redesigned voted stats view with checkmark badge
- Fixed text truncation on non-subscriber view
- Added comprehensive previews for all widget types

Bug fix:
- Voting header now updates when mood image style changes

🤖 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-21 19:06:05 -06:00
parent e1f7525d47
commit 2a703a8969
6 changed files with 1435 additions and 87 deletions

View File

@@ -222,7 +222,6 @@ struct FeelsVoteWidgetEntryView: View {
struct VotingView: View {
let family: WidgetFamily
let promptText: String
let moods: [Mood] = [.horrible, .bad, .average, .good, .great]
private var moodTint: MoodTintable.Type {
UserDefaultsStore.moodTintable()
@@ -233,34 +232,79 @@ struct VotingView: View {
}
var body: some View {
VStack(spacing: 12) {
if family == .systemSmall {
smallLayout
} else {
mediumLayout
}
}
// MARK: - Small Widget: 3 over 2 grid
private var smallLayout: some View {
VStack(spacing: 0) {
Text(promptText)
.font(.caption)
.foregroundStyle(.primary)
.multilineTextAlignment(.center)
.lineLimit(1)
.minimumScaleFactor(0.7)
.padding(.bottom, 10)
// Top row: Great, Good, Average
HStack(spacing: 12) {
ForEach([Mood.great, .good, .average], id: \.rawValue) { mood in
moodButton(for: mood, size: 36)
}
}
.padding(.bottom, 6)
// Bottom row: Bad, Horrible
HStack(spacing: 12) {
ForEach([Mood.bad, .horrible], id: \.rawValue) { mood in
moodButton(for: mood, size: 36)
}
}
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
// MARK: - Medium Widget: Single row
private var mediumLayout: some View {
VStack {
Text(promptText)
.font(.headline)
.foregroundStyle(.primary)
.multilineTextAlignment(.center)
.lineLimit(2)
.minimumScaleFactor(0.8)
.padding(.bottom, 20)
HStack(spacing: 8) {
ForEach(moods, id: \.rawValue) { mood in
Button(intent: VoteMoodIntent(mood: mood)) {
moodImages.icon(forMood: mood)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: family == .systemSmall ? 36 : 44, height: family == .systemSmall ? 36 : 44)
.foregroundColor(moodTint.color(forMood: mood))
}
.buttonStyle(.plain)
HStack(spacing: 16) {
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
moodButton(for: mood, size: 44)
}
}
}
.padding()
}
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))
}
.buttonStyle(.plain)
}
}
// MARK: - Voted Stats View (shown after voting)
struct VotedStatsView: View {
@Environment(\.widgetFamily) var family
let entry: VoteWidgetEntry
private var moodTint: MoodTintable.Type {
@@ -272,51 +316,110 @@ struct VotedStatsView: View {
}
var body: some View {
VStack(spacing: 12) {
// Today's mood
if family == .systemSmall {
smallLayout
} else {
mediumLayout
}
}
// MARK: - Small: Centered mood with checkmark
private var smallLayout: some View {
VStack(spacing: 8) {
if let mood = entry.todaysMood {
HStack(spacing: 8) {
// Large centered mood icon
ZStack(alignment: .bottomTrailing) {
moodImages.icon(forMood: mood)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 32, height: 32)
.frame(width: 56, height: 56)
.foregroundColor(moodTint.color(forMood: mood))
VStack(alignment: .leading, spacing: 2) {
Text("Today")
.font(.caption)
.foregroundStyle(.secondary)
Text(mood.widgetDisplayName)
.font(.headline)
.foregroundColor(moodTint.color(forMood: mood))
}
// Checkmark badge
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 18))
.foregroundColor(.green)
.background(Circle().fill(.white).frame(width: 14, height: 14))
.offset(x: 4, y: 4)
}
Spacer()
Text("Logged!")
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
if let stats = entry.stats {
Text("\(stats.totalEntries) day streak")
.font(.caption2)
.foregroundStyle(.tertiary)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(12)
}
// Stats
if let stats = entry.stats {
Divider()
// MARK: - Medium: Mood + stats bar
private var mediumLayout: some View {
HStack(spacing: 20) {
if let mood = entry.todaysMood {
// Left: Mood display
VStack(spacing: 6) {
moodImages.icon(forMood: mood)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 48, height: 48)
.foregroundColor(moodTint.color(forMood: mood))
VStack(spacing: 4) {
Text("\(stats.totalEntries) entries")
.font(.caption)
Text(mood.widgetDisplayName)
.font(.subheadline.weight(.semibold))
.foregroundColor(moodTint.color(forMood: mood))
Text("Today")
.font(.caption2)
.foregroundStyle(.secondary)
}
GeometryReader { geo in
HStack(spacing: 2) {
// Right: Stats
if let stats = entry.stats {
VStack(alignment: .leading, spacing: 8) {
Text("\(stats.totalEntries) entries")
.font(.caption.weight(.medium))
.foregroundStyle(.primary)
// Mini mood breakdown
HStack(spacing: 6) {
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
let percentage = stats.percentage(for: mood)
if percentage > 0 {
RoundedRectangle(cornerRadius: 2)
.fill(moodTint.color(forMood: mood))
.frame(width: max(4, geo.size.width * CGFloat(percentage) / 100))
let count = stats.moodCounts[mood, default: 0]
if count > 0 {
HStack(spacing: 2) {
Circle()
.fill(moodTint.color(forMood: mood))
.frame(width: 8, height: 8)
Text("\(count)")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
}
// Progress bar
GeometryReader { geo in
HStack(spacing: 1) {
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
let percentage = stats.percentage(for: mood)
if percentage > 0 {
RoundedRectangle(cornerRadius: 2)
.fill(moodTint.color(forMood: mood))
.frame(width: max(4, geo.size.width * CGFloat(percentage) / 100))
}
}
}
}
.frame(height: 8)
.clipShape(RoundedRectangle(cornerRadius: 4))
}
.frame(height: 12)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
@@ -335,15 +438,15 @@ struct NonSubscriberView: View {
.foregroundStyle(.pink)
Text("Track Your Mood")
.font(.headline)
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
.minimumScaleFactor(0.8)
Text("Tap to subscribe")
.font(.caption)
.font(.caption2)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.padding()
.padding(12)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}