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:
Trey t
2026-01-07 15:34:27 -06:00
parent 8ec8ed02b1
commit 40a6f879e3
13 changed files with 2429 additions and 745 deletions

View File

@@ -9,6 +9,7 @@ import MapKit
struct TripDetailView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.colorScheme) private var colorScheme
let trip: Trip
let games: [UUID: RichGame]
@@ -29,28 +30,36 @@ struct TripDetailView: View {
var body: some View {
ScrollView {
VStack(spacing: 20) {
// Header
tripHeader
VStack(spacing: 0) {
// Hero Map
heroMapSection
.frame(height: 280)
// Score Card
if let score = trip.score {
scoreCard(score)
// Content
VStack(spacing: Theme.Spacing.lg) {
// Header
tripHeader
.padding(.top, Theme.Spacing.lg)
// Stats Row
statsRow
// Score Card
if let score = trip.score {
scoreCard(score)
}
// Day-by-Day Itinerary
itinerarySection
}
// Stats
statsGrid
// Map Preview
mapPreview
// Day-by-Day Itinerary
itinerarySection
.padding(.horizontal, Theme.Spacing.lg)
.padding(.bottom, Theme.Spacing.xxl)
}
.padding()
}
.background(Theme.backgroundGradient(colorScheme))
.navigationTitle(trip.name)
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(Theme.cardBackground(colorScheme), for: .navigationBar)
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button {
@@ -59,6 +68,7 @@ struct TripDetailView: View {
}
} label: {
Image(systemName: "square.and.arrow.up")
.foregroundStyle(Theme.warmOrange)
}
Menu {
@@ -78,6 +88,7 @@ struct TripDetailView: View {
.disabled(isSaved)
} label: {
Image(systemName: "ellipsis.circle")
.foregroundStyle(Theme.warmOrange)
}
}
}
@@ -103,136 +114,215 @@ struct TripDetailView: View {
}
}
// MARK: - Hero Map Section
private var heroMapSection: some View {
ZStack(alignment: .bottom) {
Map(position: $mapCameraPosition) {
ForEach(stopCoordinates.indices, id: \.self) { index in
let stop = stopCoordinates[index]
Annotation(stop.name, coordinate: stop.coordinate) {
PulsingDot(color: index == 0 ? Theme.warmOrange : Theme.routeGold, size: 10)
}
}
ForEach(routePolylines.indices, id: \.self) { index in
MapPolyline(routePolylines[index])
.stroke(Theme.routeGold, lineWidth: 4)
}
}
.mapStyle(colorScheme == .dark ? .standard(elevation: .flat, emphasis: .muted) : .standard)
// Gradient overlay at bottom
LinearGradient(
colors: [.clear, Theme.cardBackground(colorScheme).opacity(0.8), Theme.cardBackground(colorScheme)],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 80)
// Loading indicator
if isLoadingRoutes {
ProgressView()
.tint(Theme.warmOrange)
.padding(.bottom, 40)
}
}
.task {
updateMapRegion()
await fetchDrivingRoutes()
}
}
// MARK: - Header
private var tripHeader: some View {
VStack(alignment: .leading, spacing: 8) {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Date range
Text(trip.formattedDateRange)
.font(.subheadline)
.foregroundStyle(.secondary)
.font(.system(size: Theme.FontSize.caption, weight: .medium))
.foregroundStyle(Theme.textSecondary(colorScheme))
HStack(spacing: 16) {
// Route preview
RoutePreviewStrip(cities: trip.stops.map { $0.city })
.padding(.vertical, Theme.Spacing.xs)
// Sport badges
HStack(spacing: Theme.Spacing.xs) {
ForEach(Array(trip.uniqueSports), id: \.self) { sport in
Label(sport.rawValue, systemImage: sport.iconName)
.font(.caption)
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(Color.blue.opacity(0.1))
.clipShape(Capsule())
HStack(spacing: 4) {
Image(systemName: sport.iconName)
.font(.system(size: 10))
Text(sport.rawValue)
.font(.system(size: 11, weight: .medium))
}
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(sport.themeColor.opacity(0.2))
.foregroundStyle(sport.themeColor)
.clipShape(Capsule())
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
// MARK: - Stats Row
private var statsRow: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Theme.Spacing.sm) {
StatPill(icon: "calendar", value: "\(trip.tripDuration) days")
StatPill(icon: "mappin.circle", value: "\(trip.stops.count) cities")
StatPill(icon: "sportscourt", value: "\(trip.totalGames) games")
StatPill(icon: "road.lanes", value: trip.formattedTotalDistance)
StatPill(icon: "car", value: trip.formattedTotalDriving)
}
}
}
// MARK: - Score Card
private func scoreCard(_ score: TripScore) -> some View {
VStack(spacing: 12) {
VStack(spacing: Theme.Spacing.md) {
HStack {
Text("Trip Score")
.font(.headline)
.font(.system(size: Theme.FontSize.cardTitle, weight: .semibold))
.foregroundStyle(Theme.textPrimary(colorScheme))
Spacer()
Text(score.scoreGrade)
.font(.largeTitle)
.fontWeight(.bold)
.foregroundStyle(.green)
.font(.system(size: 32, weight: .bold, design: .rounded))
.foregroundStyle(Theme.warmOrange)
.glowEffect(color: Theme.warmOrange, radius: 8)
}
HStack(spacing: 20) {
scoreItem(label: "Games", value: score.gameQualityScore)
scoreItem(label: "Route", value: score.routeEfficiencyScore)
scoreItem(label: "Balance", value: score.leisureBalanceScore)
scoreItem(label: "Prefs", value: score.preferenceAlignmentScore)
HStack(spacing: Theme.Spacing.lg) {
scoreItem(label: "Games", value: score.gameQualityScore, color: Theme.mlbRed)
scoreItem(label: "Route", value: score.routeEfficiencyScore, color: Theme.routeGold)
scoreItem(label: "Balance", value: score.leisureBalanceScore, color: Theme.mlsGreen)
scoreItem(label: "Prefs", value: score.preferenceAlignmentScore, color: Theme.nbaOrange)
}
}
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
.cardStyle()
}
private func scoreItem(label: String, value: Double) -> some View {
private func scoreItem(label: String, value: Double, color: Color) -> some View {
VStack(spacing: 4) {
Text(String(format: "%.0f", value))
.font(.headline)
.font(.system(size: Theme.FontSize.cardTitle, weight: .bold))
.foregroundStyle(color)
Text(label)
.font(.caption2)
.foregroundStyle(.secondary)
.font(.system(size: Theme.FontSize.micro))
.foregroundStyle(Theme.textMuted(colorScheme))
}
}
// MARK: - Stats Grid
// MARK: - Itinerary
private var statsGrid: some View {
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 16) {
statCell(value: "\(trip.tripDuration)", label: "Days", icon: "calendar")
statCell(value: "\(trip.stops.count)", label: "Cities", icon: "mappin.circle")
statCell(value: "\(trip.totalGames)", label: "Games", icon: "sportscourt")
statCell(value: trip.formattedTotalDistance, label: "Distance", icon: "road.lanes")
statCell(value: trip.formattedTotalDriving, label: "Driving", icon: "car")
statCell(value: String(format: "%.1fh", trip.averageDrivingHoursPerDay), label: "Avg/Day", icon: "gauge.medium")
}
}
private var itinerarySection: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.md) {
Text("Itinerary")
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
.foregroundStyle(Theme.textPrimary(colorScheme))
private func statCell(value: String, label: String, icon: String) -> some View {
VStack(spacing: 6) {
Image(systemName: icon)
.font(.title2)
.foregroundStyle(.blue)
Text(value)
.font(.headline)
Text(label)
.font(.caption)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
// MARK: - Map Preview
private var mapPreview: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Route")
.font(.headline)
Spacer()
if isLoadingRoutes {
ProgressView()
.scaleEffect(0.7)
ForEach(Array(itinerarySections.enumerated()), id: \.offset) { index, section in
switch section {
case .day(let dayNumber, let date, let gamesOnDay):
DaySection(
dayNumber: dayNumber,
date: date,
games: gamesOnDay
)
.staggeredAnimation(index: index)
case .travel(let segment):
TravelSection(segment: segment)
.staggeredAnimation(index: index)
}
}
Map(position: $mapCameraPosition) {
// Add markers for each stop
ForEach(stopCoordinates.indices, id: \.self) { index in
let stop = stopCoordinates[index]
Marker(stop.name, coordinate: stop.coordinate)
.tint(.blue)
}
// Add actual driving route polylines
ForEach(routePolylines.indices, id: \.self) { index in
MapPolyline(routePolylines[index])
.stroke(.blue, lineWidth: 3)
}
}
.frame(height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
.task {
updateMapRegion()
await fetchDrivingRoutes()
}
}
}
/// Fetch actual driving routes using MKDirections
/// Build itinerary sections: days and travel between days
private var itinerarySections: [ItinerarySection] {
var sections: [ItinerarySection] = []
let calendar = Calendar.current
let days = tripDays
for (index, dayDate) in days.enumerated() {
let dayNum = index + 1
let gamesOnDay = gamesOn(date: dayDate)
if !gamesOnDay.isEmpty || index == 0 || index == days.count - 1 {
sections.append(.day(dayNumber: dayNum, date: dayDate, games: gamesOnDay))
}
let travelAfterDay = travelDepartingAfter(date: dayDate, beforeNextGameDay: days.indices.contains(index + 1) ? days[index + 1] : nil)
for segment in travelAfterDay {
sections.append(.travel(segment))
}
}
return sections
}
private var tripDays: [Date] {
let calendar = Calendar.current
guard let startDate = trip.stops.first?.arrivalDate,
let endDate = trip.stops.last?.departureDate else { return [] }
var days: [Date] = []
var current = calendar.startOfDay(for: startDate)
let end = calendar.startOfDay(for: endDate)
while current <= end {
days.append(current)
current = calendar.date(byAdding: .day, value: 1, to: current)!
}
return days
}
private func gamesOn(date: Date) -> [RichGame] {
let calendar = Calendar.current
let dayStart = calendar.startOfDay(for: date)
let allGameIds = trip.stops.flatMap { $0.games }
return allGameIds.compactMap { games[$0] }.filter { richGame in
calendar.startOfDay(for: richGame.game.dateTime) == dayStart
}.sorted { $0.game.dateTime < $1.game.dateTime }
}
private func travelDepartingAfter(date: Date, beforeNextGameDay: Date?) -> [TravelSegment] {
let calendar = Calendar.current
let dayEnd = calendar.startOfDay(for: date)
return trip.travelSegments.filter { segment in
let segmentDay = calendar.startOfDay(for: segment.departureTime)
return segmentDay == dayEnd
}
}
// MARK: - Map Helpers
private func fetchDrivingRoutes() async {
let stops = stopCoordinates
guard stops.count >= 2 else { return }
@@ -257,8 +347,6 @@ struct TripDetailView: View {
polylines.append(route.polyline)
}
} catch {
// Fallback to straight line if directions fail
print("Failed to get directions from \(source.name) to \(destination.name): \(error)")
let straightLine = MKPolyline(coordinates: [source.coordinate, destination.coordinate], count: 2)
polylines.append(straightLine)
}
@@ -268,14 +356,11 @@ struct TripDetailView: View {
isLoadingRoutes = false
}
/// Get coordinates for all stops (from stop coordinate or stadium)
private var stopCoordinates: [(name: String, coordinate: CLLocationCoordinate2D)] {
trip.stops.compactMap { stop -> (String, CLLocationCoordinate2D)? in
// First try to use the stop's stored coordinate
if let coord = stop.coordinate {
return (stop.city, coord)
}
// Fall back to stadium coordinate if available
if let stadiumId = stop.stadium,
let stadium = dataProvider.stadium(for: stadiumId) {
return (stadium.name, stadium.coordinate)
@@ -284,14 +369,6 @@ struct TripDetailView: View {
}
}
/// Resolved stadiums from trip stops (for markers)
private var tripStadiums: [Stadium] {
trip.stops.compactMap { stop in
guard let stadiumId = stop.stadium else { return nil }
return dataProvider.stadium(for: stadiumId)
}
}
private func updateMapRegion() {
guard !stopCoordinates.isEmpty else { return }
@@ -309,7 +386,6 @@ struct TripDetailView: View {
longitude: (minLon + maxLon) / 2
)
// Add padding to the span
let latSpan = (maxLat - minLat) * 1.3 + 0.5
let lonSpan = (maxLon - minLon) * 1.3 + 0.5
@@ -319,99 +395,6 @@ struct TripDetailView: View {
))
}
// MARK: - Itinerary
private var itinerarySection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Itinerary")
.font(.headline)
ForEach(itinerarySections.indices, id: \.self) { index in
let section = itinerarySections[index]
switch section {
case .day(let dayNumber, let date, let gamesOnDay):
DaySection(
dayNumber: dayNumber,
date: date,
games: gamesOnDay
)
case .travel(let segment):
TravelSection(segment: segment)
}
}
}
}
/// Build itinerary sections: days and travel between days
private var itinerarySections: [ItinerarySection] {
var sections: [ItinerarySection] = []
let calendar = Calendar.current
// Get all days
let days = tripDays
for (index, dayDate) in days.enumerated() {
let dayNum = index + 1
let gamesOnDay = gamesOn(date: dayDate)
// Add day section (even if no games - could be rest day)
if !gamesOnDay.isEmpty || index == 0 || index == days.count - 1 {
sections.append(.day(dayNumber: dayNum, date: dayDate, games: gamesOnDay))
}
// Check for travel AFTER this day (between this day and next)
let travelAfterDay = travelDepartingAfter(date: dayDate, beforeNextGameDay: days.indices.contains(index + 1) ? days[index + 1] : nil)
for segment in travelAfterDay {
sections.append(.travel(segment))
}
}
return sections
}
/// All calendar days in the trip
private var tripDays: [Date] {
let calendar = Calendar.current
guard let startDate = trip.stops.first?.arrivalDate,
let endDate = trip.stops.last?.departureDate else { return [] }
var days: [Date] = []
var current = calendar.startOfDay(for: startDate)
let end = calendar.startOfDay(for: endDate)
while current <= end {
days.append(current)
current = calendar.date(byAdding: .day, value: 1, to: current)!
}
return days
}
/// Games scheduled on a specific date
private func gamesOn(date: Date) -> [RichGame] {
let calendar = Calendar.current
let dayStart = calendar.startOfDay(for: date)
// Get all game IDs from all stops
let allGameIds = trip.stops.flatMap { $0.games }
return allGameIds.compactMap { games[$0] }.filter { richGame in
calendar.startOfDay(for: richGame.game.dateTime) == dayStart
}.sorted { $0.game.dateTime < $1.game.dateTime }
}
/// Travel segments that depart after a given day (for between-day travel)
private func travelDepartingAfter(date: Date, beforeNextGameDay: Date?) -> [TravelSegment] {
let calendar = Calendar.current
let dayEnd = calendar.startOfDay(for: date)
return trip.travelSegments.filter { segment in
let segmentDay = calendar.startOfDay(for: segment.departureTime)
// Travel is "after" this day if it departs on or after this day
// and arrives at a different city
return segmentDay == dayEnd
}
}
// MARK: - Actions
private func exportPDF() async {
@@ -430,7 +413,7 @@ struct TripDetailView: View {
}
private func saveTrip() {
guard let savedTrip = SavedTrip.from(trip, status: .planned) else {
guard let savedTrip = SavedTrip.from(trip, games: games, status: .planned) else {
print("Failed to create SavedTrip")
return
}
@@ -458,24 +441,23 @@ struct TripDetailView: View {
}
}
// MARK: - Itinerary Section (enum for day vs travel sections)
// MARK: - Itinerary Section
enum ItinerarySection {
case day(dayNumber: Int, date: Date, games: [RichGame])
case travel(TravelSegment)
}
// MARK: - Day Section (header + games)
// MARK: - Day Section
struct DaySection: View {
let dayNumber: Int
let date: Date
let games: [RichGame]
@Environment(\.colorScheme) private var colorScheme
private var formattedDate: String {
let formatter = DateFormatter()
formatter.dateFormat = "EEEE, MMM d"
return formatter.string(from: date)
date.formatted(.dateTime.weekday(.wide).month().day())
}
private var gameCity: String? {
@@ -487,105 +469,151 @@ struct DaySection: View {
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Day header
HStack {
Text("Day \(dayNumber)")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundStyle(.blue)
VStack(alignment: .leading, spacing: 2) {
Text("Day \(dayNumber)")
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
.foregroundStyle(Theme.textPrimary(colorScheme))
Text(formattedDate)
.font(.subheadline)
.foregroundStyle(.secondary)
Text(formattedDate)
.font(.system(size: Theme.FontSize.caption, weight: .medium))
.foregroundStyle(Theme.textSecondary(colorScheme))
}
Spacer()
if isRestDay {
Text("Rest Day")
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(Color.green.opacity(0.2))
.clipShape(Capsule())
.badgeStyle(color: Theme.mlsGreen, filled: false)
} else if !games.isEmpty {
Text("\(games.count) game\(games.count > 1 ? "s" : "")")
.badgeStyle(color: Theme.warmOrange, filled: false)
}
}
// City label (if games exist)
// City label
if let city = gameCity {
Label(city, systemImage: "mappin")
.font(.caption)
.foregroundStyle(.secondary)
.font(.system(size: Theme.FontSize.caption))
.foregroundStyle(Theme.textSecondary(colorScheme))
}
// Games
ForEach(games, id: \.game.id) { richGame in
HStack {
Image(systemName: richGame.game.sport.iconName)
.foregroundStyle(.blue)
.frame(width: 20)
Text(richGame.matchupDescription)
.font(.subheadline)
Spacer()
Text(richGame.game.gameTime)
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(Color(.tertiarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 8))
GameRow(game: richGame)
}
}
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
.cardStyle()
}
}
// MARK: - Travel Section (standalone travel between days)
// MARK: - Game Row
struct GameRow: View {
let game: RichGame
@Environment(\.colorScheme) private var colorScheme
var body: some View {
HStack(spacing: Theme.Spacing.md) {
// Sport color bar
SportColorBar(sport: game.game.sport)
VStack(alignment: .leading, spacing: 4) {
// Matchup
HStack(spacing: 4) {
Text(game.awayTeam.abbreviation)
.font(.system(size: Theme.FontSize.body, weight: .bold))
Text("@")
.foregroundStyle(Theme.textMuted(colorScheme))
Text(game.homeTeam.abbreviation)
.font(.system(size: Theme.FontSize.body, weight: .bold))
}
.foregroundStyle(Theme.textPrimary(colorScheme))
// Stadium
HStack(spacing: 4) {
Image(systemName: "building.2")
.font(.system(size: 10))
Text(game.stadium.name)
.font(.system(size: Theme.FontSize.caption))
}
.foregroundStyle(Theme.textSecondary(colorScheme))
}
Spacer()
// Time
Text(game.game.gameTime)
.font(.system(size: Theme.FontSize.caption, weight: .semibold))
.foregroundStyle(Theme.warmOrange)
}
.padding(Theme.Spacing.sm)
.background(Theme.cardBackgroundElevated(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.small))
}
}
// MARK: - Travel Section
struct TravelSection: View {
let segment: TravelSegment
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Travel header
Text("Travel")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundStyle(.orange)
VStack(spacing: 0) {
// Top connector
Rectangle()
.fill(Theme.routeGold.opacity(0.4))
.frame(width: 2, height: 16)
// Travel details
HStack(spacing: 8) {
Image(systemName: segment.travelMode.iconName)
.foregroundStyle(.orange)
.frame(width: 20)
// Travel card
HStack(spacing: Theme.Spacing.md) {
// Icon
ZStack {
Circle()
.fill(Theme.cardBackgroundElevated(colorScheme))
.frame(width: 44, height: 44)
Image(systemName: "car.fill")
.foregroundStyle(Theme.routeGold)
}
VStack(alignment: .leading, spacing: 2) {
Text("\(segment.fromLocation.name)\(segment.toLocation.name)")
.font(.subheadline)
.fontWeight(.medium)
Text("Travel")
.font(.system(size: Theme.FontSize.micro, weight: .semibold))
.foregroundStyle(Theme.textMuted(colorScheme))
Text("\(segment.formattedDistance) \(segment.formattedDuration)")
.font(.caption)
.foregroundStyle(.secondary)
Text("\(segment.fromLocation.name) \(segment.toLocation.name)")
.font(.system(size: Theme.FontSize.body, weight: .medium))
.foregroundStyle(Theme.textPrimary(colorScheme))
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text(segment.formattedDistance)
.font(.system(size: Theme.FontSize.caption, weight: .semibold))
.foregroundStyle(Theme.textPrimary(colorScheme))
Text(segment.formattedDuration)
.font(.system(size: Theme.FontSize.micro))
.foregroundStyle(Theme.textSecondary(colorScheme))
}
}
.padding(.vertical, 8)
.padding(.horizontal, 10)
.background(Color.orange.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 8))
.padding(Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme).opacity(0.7))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.strokeBorder(Theme.routeGold.opacity(0.3), lineWidth: 1)
}
// Bottom connector
Rectangle()
.fill(Theme.routeGold.opacity(0.4))
.frame(width: 2, height: 16)
}
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.orange.opacity(0.3), lineWidth: 1)
)
}
}