diff --git a/MealMood/Resources/de.lproj/Localizable.strings b/MealMood/Resources/de.lproj/Localizable.strings
index 60eae50..47e52a9 100644
--- a/MealMood/Resources/de.lproj/Localizable.strings
+++ b/MealMood/Resources/de.lproj/Localizable.strings
@@ -460,3 +460,5 @@
"violations_replace" = "Gericht ändern";
"stats_tag_by_meal_title" = "Tags nach Mahlzeit";
"stats_dishes_per_tag_title" = "Gerichte pro Tag";
+"violations_banner_title" = "%d Mahlzeit(en) verletzen deine Regeln";
+"violations_banner_cta" = "Ansehen";
diff --git a/MealMood/Resources/en.lproj/Localizable.strings b/MealMood/Resources/en.lproj/Localizable.strings
index ad999e0..0ddd1bb 100644
--- a/MealMood/Resources/en.lproj/Localizable.strings
+++ b/MealMood/Resources/en.lproj/Localizable.strings
@@ -460,3 +460,5 @@
"violations_replace" = "Change dish";
"stats_tag_by_meal_title" = "Tags by meal type";
"stats_dishes_per_tag_title" = "Dishes per tag";
+"violations_banner_title" = "%d meal(s) break your rules";
+"violations_banner_cta" = "View";
diff --git a/MealMood/Resources/es.lproj/Localizable.strings b/MealMood/Resources/es.lproj/Localizable.strings
index c0567e0..2e904ac 100644
--- a/MealMood/Resources/es.lproj/Localizable.strings
+++ b/MealMood/Resources/es.lproj/Localizable.strings
@@ -460,3 +460,5 @@
"violations_replace" = "Cambiar plato";
"stats_tag_by_meal_title" = "Etiquetas por tipo de comida";
"stats_dishes_per_tag_title" = "Platos por etiqueta";
+"violations_banner_title" = "%d comida(s) incumplen tus reglas";
+"violations_banner_cta" = "Ver";
diff --git a/MealMood/Resources/fr.lproj/Localizable.strings b/MealMood/Resources/fr.lproj/Localizable.strings
index da22f29..f30b83d 100644
--- a/MealMood/Resources/fr.lproj/Localizable.strings
+++ b/MealMood/Resources/fr.lproj/Localizable.strings
@@ -460,3 +460,5 @@
"violations_replace" = "Changer de plat";
"stats_tag_by_meal_title" = "Étiquettes par type de repas";
"stats_dishes_per_tag_title" = "Plats par étiquette";
+"violations_banner_title" = "%d repas ne respecte(nt) pas vos règles";
+"violations_banner_cta" = "Voir";
diff --git a/MealMood/Resources/it.lproj/Localizable.strings b/MealMood/Resources/it.lproj/Localizable.strings
index a92b3ba..9008f23 100644
--- a/MealMood/Resources/it.lproj/Localizable.strings
+++ b/MealMood/Resources/it.lproj/Localizable.strings
@@ -460,3 +460,5 @@
"violations_replace" = "Cambia piatto";
"stats_tag_by_meal_title" = "Etichette per tipo di pasto";
"stats_dishes_per_tag_title" = "Piatti per etichetta";
+"violations_banner_title" = "%d pasto/i non rispettano le tue regole";
+"violations_banner_cta" = "Vedi";
diff --git a/MealMood/Resources/pt-BR.lproj/Localizable.strings b/MealMood/Resources/pt-BR.lproj/Localizable.strings
index ba89f85..c220807 100644
--- a/MealMood/Resources/pt-BR.lproj/Localizable.strings
+++ b/MealMood/Resources/pt-BR.lproj/Localizable.strings
@@ -460,3 +460,5 @@
"violations_replace" = "Trocar prato";
"stats_tag_by_meal_title" = "Etiquetas por tipo de refeição";
"stats_dishes_per_tag_title" = "Pratos por etiqueta";
+"violations_banner_title" = "%d refeição(ões) quebram suas regras";
+"violations_banner_cta" = "Ver";
diff --git a/MealMood/Views/Home/HomeView.swift b/MealMood/Views/Home/HomeView.swift
index 4e6a785..abb300e 100644
--- a/MealMood/Views/Home/HomeView.swift
+++ b/MealMood/Views/Home/HomeView.swift
@@ -317,11 +317,73 @@ struct HomeView: View {
.padding(.bottom, 10)
}
+ /// Banner shown on the home whenever the week breaks rules. The panel used
+ /// to be reachable only from the "···" menu, so violations went unnoticed.
+ @ViewBuilder
+ private func violationsBanner(plan: WeekPlan) -> some View {
+ let violations = AutocompleteEngine.findViolations(plan: plan, allDishes: dishes, allTags: tags)
+ if !violations.isEmpty {
+ Button {
+ showViolationsPanel = true
+ } label: {
+ HStack(spacing: 10) {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .font(.system(size: 16))
+ .foregroundColor(.mealMoodWarning)
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(String(format: String(localized: "violations_banner_title"), violations.count))
+ .font(.mealMoodSmall.weight(.semibold))
+ .foregroundColor(.mealMoodTextPrimary)
+ Text(violationsSummary(violations))
+ .font(.mealMoodCaption)
+ .foregroundColor(.mealMoodTextSecondary)
+ .lineLimit(1)
+ }
+
+ Spacer()
+
+ Text("violations_banner_cta")
+ .font(.mealMoodCaption.weight(.semibold))
+ .foregroundColor(.white)
+ .padding(.horizontal, 10)
+ .padding(.vertical, 5)
+ .background(Color.mealMoodWarning)
+ .clipShape(Capsule())
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 10)
+ .background(Color.mealMoodWarning.opacity(0.12))
+ .clipShape(RoundedRectangle(cornerRadius: 12))
+ .overlay(
+ RoundedRectangle(cornerRadius: 12)
+ .stroke(Color.mealMoodWarning.opacity(0.45), lineWidth: 1)
+ )
+ }
+ .buttonStyle(.plain)
+ .padding(.horizontal, 16)
+ .padding(.bottom, 8)
+ }
+ }
+
+ /// "Lun · Cena, Mié · Comida" — where the problems are, at a glance.
+ private func violationsSummary(_ violations: [AutocompleteEngine.RuleViolation]) -> String {
+ let language = settings?.languageEnum.resolved() ?? .spanish
+ let keys = ["day_mon","day_tue","day_wed","day_thu","day_fri","day_sat","day_sun"]
+ return violations.prefix(3).map { violation in
+ let day = violation.dayOfWeek >= 0 && violation.dayOfWeek < keys.count
+ ? localizedString(keys[violation.dayOfWeek], language: language) : ""
+ return "\(day) · \(localizedString(violation.mealType, language: language))"
+ }.joined(separator: ", ")
+ }
+
@ViewBuilder
private func mainContent(plan: WeekPlan, settings: AppSettings) -> some View {
VStack(spacing: 0) {
weekSwitcher(settings: settings)
+ violationsBanner(plan: plan)
+
if showWeekLimitUpsell && !settings.isPremium {
PremiumUpsellBanner(
messageKey: "premium_limit_future_weeks",
diff --git a/MealMoodUITests/OnboardingFlowUITests.swift b/MealMoodUITests/OnboardingFlowUITests.swift
index 37e72af..1f31445 100644
--- a/MealMoodUITests/OnboardingFlowUITests.swift
+++ b/MealMoodUITests/OnboardingFlowUITests.swift
@@ -111,3 +111,4 @@ final class NextWeekNavigationUITests: XCTestCase {
}
}
+
diff --git a/MealMoodWatchWidget/Info.plist b/MealMoodWatchWidget/Info.plist
index f8407dd..4d14ec2 100644
--- a/MealMoodWatchWidget/Info.plist
+++ b/MealMoodWatchWidget/Info.plist
@@ -17,7 +17,7 @@
CFBundleShortVersionString
2.1.1
CFBundleVersion
- 86
+ 87
NSExtension
NSExtensionPointIdentifier
diff --git a/MealMoodWidget/Info.plist b/MealMoodWidget/Info.plist
index f8407dd..4d14ec2 100644
--- a/MealMoodWidget/Info.plist
+++ b/MealMoodWidget/Info.plist
@@ -17,7 +17,7 @@
CFBundleShortVersionString
2.1.1
CFBundleVersion
- 86
+ 87
NSExtension
NSExtensionPointIdentifier