crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit f609b91f4d569f3b0d7072306689ce574ace15b3
parent 8df7024aef31b2948138571a570e7b93fd88a661
Author: Michael Camilleri <[email protected]>
Date:   Fri, 22 May 2026 01:44:52 +0900

Add icons to announcement banners

Diffstat:
MCrossmate/Views/AnnouncementBanner.swift | 60+++++++++++++++++++++++++++++++++++++++++++-----------------
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 + } + } }