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>
This commit is contained in:
220
SportsTime/Core/Theme/ViewModifiers.swift
Normal file
220
SportsTime/Core/Theme/ViewModifiers.swift
Normal file
@@ -0,0 +1,220 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user