commit f609b91f4d569f3b0d7072306689ce574ace15b3
parent 8df7024aef31b2948138571a570e7b93fd88a661
Author: Michael Camilleri <[email protected]>
Date: Fri, 22 May 2026 01:44:52 +0900
Add icons to announcement banners
Diffstat:
1 file changed, 43 insertions(+), 17 deletions(-)
diff --git a/Crossmate/Views/AnnouncementBanner.swift b/Crossmate/Views/AnnouncementBanner.swift
@@ -1,26 +1,34 @@
import SwiftUI
-/// Banner-style surface for an `Announcement`. Renders body text with a
-/// severity-tinted background, taps to dismiss when `.manual`, and is
-/// intended to live in the puzzle header (and eventually the game list)
-/// behind a `.transition(.move(edge: .top))` driven by the parent.
+/// Banner-style surface for an `Announcement`. Renders a severity icon and
+/// body text over a severity-tinted background, taps to dismiss when
+/// `.manual`, and is intended to live in the puzzle header (and eventually
+/// the game list) behind a `.transition(.move(edge: .top))` driven by the
+/// parent.
struct AnnouncementBanner: View {
- @Environment(PlayerPreferences.self) private var preferences
let announcement: Announcement
let onDismiss: (() -> Void)?
var body: some View {
let tint = backgroundTint(for: announcement.severity)
- VStack(spacing: 2) {
- if let title = announcement.title, !title.isEmpty {
- Text(title)
- .font(.subheadline.weight(.semibold))
- .lineLimit(1)
+ HStack(spacing: 10) {
+ Image(systemName: iconName(for: announcement.severity))
+ // `.title` (~28pt) against the `.subheadline` (~15pt) body
+ // text — roughly 1.9× — so the severity reads at a glance.
+ .font(.title.weight(.semibold))
+ .foregroundStyle(iconTint(for: announcement.severity))
+ .accessibilityHidden(true)
+ VStack(spacing: 2) {
+ if let title = announcement.title, !title.isEmpty {
+ Text(title)
+ .font(.subheadline.weight(.semibold))
+ .lineLimit(1)
+ }
+ Text(announcement.body)
+ .font(.subheadline)
+ .multilineTextAlignment(.center)
+ .lineLimit(3)
}
- Text(announcement.body)
- .font(.subheadline)
- .multilineTextAlignment(.center)
- .lineLimit(3)
}
.frame(maxWidth: .infinity, alignment: .center)
.padding(.horizontal, 14)
@@ -38,11 +46,29 @@ struct AnnouncementBanner: View {
private func backgroundTint(for severity: Announcement.Severity) -> Color {
switch severity {
- // Match the Clue Bar's faint author-attribution tint so info-class
- // banners read as part of the puzzle chrome rather than a system fill.
- case .info: return preferences.color.authorTintFill
+ case .info: return Color(.tertiarySystemFill)
case .warning: return Color.orange.opacity(0.18)
case .error: return Color.red.opacity(0.18)
}
}
+
+ /// SF Symbol that leads the banner text, one per severity — an attention
+ /// cue that reads at a glance even before the text does.
+ private func iconName(for severity: Announcement.Severity) -> String {
+ switch severity {
+ case .info: return "info.circle.fill"
+ case .warning: return "exclamationmark.triangle.fill"
+ case .error: return "xmark.octagon.fill"
+ }
+ }
+
+ /// Foreground colour for the severity icon — kept solid (not the faint
+ /// background opacity) so the icon stands out against `backgroundTint`.
+ private func iconTint(for severity: Announcement.Severity) -> Color {
+ switch severity {
+ case .info: return .blue
+ case .warning: return .orange
+ case .error: return .red
+ }
+ }
}