Files
Sportstime/SportsTime/Core/Theme/ViewModifiers.swift
Trey t 40a6f879e3 UI overhaul: new color palette, trip creation improvements, crash fix
Theme:
- New teal/cyan/mint/pink/gold color palette replacing orange/cream
- Added Theme.swift, ViewModifiers.swift, AnimatedComponents.swift

Trip Creation:
- Removed Drive/Fly toggle (drive-only for now)
- Removed Lodging Type picker
- Renamed "Number of Stops" to "Number of Cities" with explanation
- Added explanation for "Find Other Sports Along Route"
- Removed staggered animation from trip options list

Bug Fix:
- Disabled AI route description generation (Foundation Models crashes
  in iOS 26.2 Simulator due to NLLanguageRecognizer assertion failure)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 15:34:27 -06:00

221 lines
5.8 KiB
Swift

//
// ViewModifiers.swift
// SportsTime
//
// Reusable view modifiers for consistent styling across the app.
//
import SwiftUI
// MARK: - Card Style Modifier
struct CardStyle: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
var cornerRadius: CGFloat = Theme.CornerRadius.large
var padding: CGFloat = Theme.Spacing.lg
func body(content: Content) -> some View {
content
.padding(padding)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
.overlay {
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.shadow(color: Theme.cardShadow(colorScheme), radius: 10, y: 5)
}
}
extension View {
func cardStyle(cornerRadius: CGFloat = Theme.CornerRadius.large, padding: CGFloat = Theme.Spacing.lg) -> some View {
modifier(CardStyle(cornerRadius: cornerRadius, padding: padding))
}
}
// MARK: - Glow Effect Modifier
struct GlowEffect: ViewModifier {
var color: Color = Theme.warmOrange
var radius: CGFloat = 8
func body(content: Content) -> some View {
content
.shadow(color: color.opacity(0.5), radius: radius / 2, y: 0)
.shadow(color: color.opacity(0.3), radius: radius, y: 0)
}
}
extension View {
func glowEffect(color: Color = Theme.warmOrange, radius: CGFloat = 8) -> some View {
modifier(GlowEffect(color: color, radius: radius))
}
}
// MARK: - Pressable Button Style
struct PressableButtonStyle: ButtonStyle {
var scale: CGFloat = 0.96
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? scale : 1.0)
.animation(Theme.Animation.spring, value: configuration.isPressed)
}
}
extension View {
func pressableStyle(scale: CGFloat = 0.96) -> some View {
buttonStyle(PressableButtonStyle(scale: scale))
}
}
// MARK: - Shimmer Effect Modifier
struct ShimmerEffect: ViewModifier {
@State private var phase: CGFloat = 0
func body(content: Content) -> some View {
content
.overlay {
GeometryReader { geo in
LinearGradient(
colors: [
.clear,
Color.white.opacity(0.3),
.clear
],
startPoint: .leading,
endPoint: .trailing
)
.frame(width: geo.size.width * 2)
.offset(x: -geo.size.width + (geo.size.width * 2 * phase))
}
.mask(content)
}
.onAppear {
withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) {
phase = 1
}
}
}
}
extension View {
func shimmer() -> some View {
modifier(ShimmerEffect())
}
}
// MARK: - Staggered Animation Modifier
struct StaggeredAnimation: ViewModifier {
var index: Int
var delay: Double = Theme.Animation.staggerDelay
@State private var appeared = false
func body(content: Content) -> some View {
content
.opacity(appeared ? 1 : 0)
.offset(y: appeared ? 0 : 20)
.onAppear {
withAnimation(Theme.Animation.spring.delay(Double(index) * delay)) {
appeared = true
}
}
}
}
extension View {
func staggeredAnimation(index: Int, delay: Double = Theme.Animation.staggerDelay) -> some View {
modifier(StaggeredAnimation(index: index, delay: delay))
}
}
// MARK: - Badge Style Modifier
struct BadgeStyle: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
var color: Color = Theme.warmOrange
var filled: Bool = true
func body(content: Content) -> some View {
content
.font(.system(size: Theme.FontSize.micro, weight: .semibold))
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(filled ? color : color.opacity(0.2))
.foregroundStyle(filled ? .white : color)
.clipShape(Capsule())
}
}
extension View {
func badgeStyle(color: Color = Theme.warmOrange, filled: Bool = true) -> some View {
modifier(BadgeStyle(color: color, filled: filled))
}
}
// MARK: - Section Header Style
struct SectionHeaderStyle: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
func body(content: Content) -> some View {
content
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
.foregroundStyle(Theme.textPrimary(colorScheme))
}
}
extension View {
func sectionHeaderStyle() -> some View {
modifier(SectionHeaderStyle())
}
}
// MARK: - Themed Background Modifier
struct ThemedBackground: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
func body(content: Content) -> some View {
content
.background(Theme.backgroundGradient(colorScheme))
}
}
extension View {
func themedBackground() -> some View {
modifier(ThemedBackground())
}
}
// MARK: - Sport Color Bar
struct SportColorBar: View {
let sport: Sport
var body: some View {
RoundedRectangle(cornerRadius: 2)
.fill(sport.themeColor)
.frame(width: 4)
}
}
// MARK: - Sport Extension for Theme Colors
extension Sport {
var themeColor: Color {
switch self {
case .mlb: return Theme.mlbRed
case .nba: return Theme.nbaOrange
case .nhl: return Theme.nhlBlue
case .nfl: return Theme.nflBrown
case .mls: return Theme.mlsGreen
}
}
}