Detailed step-by-step plan for extending brutalist style to: - TripDetailView - SavedTripsListView - ScheduleListView - SettingsView Includes StyleProvider protocol, adaptive routers, and complete code snippets for each task. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
35 KiB
Brutalist App-Wide Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Extend the Brutalist design style from home-screen-only to work app-wide (TripDetailView, ScheduleListView, SavedTripsListView, SettingsView).
Architecture: Adaptive view routers that switch between Classic and Brutalist variants based on DesignStyleManager.shared.currentStyle. StyleProvider protocol provides shared styling constants. Brutalist uses Theme colors but with monospace fonts, sharp corners, and borders.
Tech Stack: SwiftUI, @Observable pattern, Theme system, DesignStyleManager
Task 1: Create StyleProvider Protocol
Files:
- Create:
SportsTime/Core/Design/StyleProvider.swift
Step 1: Create the StyleProvider file
//
// StyleProvider.swift
// SportsTime
//
// Provides style-specific constants for shared components.
//
import SwiftUI
/// Protocol for style-specific visual properties
protocol StyleProvider {
// Shape
var cornerRadius: CGFloat { get }
var borderWidth: CGFloat { get }
// Typography
var fontDesign: Font.Design { get }
var usesUppercase: Bool { get }
var headerTracking: CGFloat { get }
// Effects
var usesGradientBackgrounds: Bool { get }
var usesSoftShadows: Bool { get }
// Helpers
func flatBackground(_ colorScheme: ColorScheme) -> Color
}
// MARK: - Classic Style
struct ClassicStyle: StyleProvider {
let cornerRadius: CGFloat = 12
let borderWidth: CGFloat = 0
let fontDesign: Font.Design = .default
let usesUppercase = false
let headerTracking: CGFloat = 0
let usesGradientBackgrounds = true
let usesSoftShadows = true
func flatBackground(_ colorScheme: ColorScheme) -> Color {
Theme.cardBackground(colorScheme)
}
}
// MARK: - Brutalist Style
struct BrutalistStyle: StyleProvider {
let cornerRadius: CGFloat = 0
let borderWidth: CGFloat = 1
let fontDesign: Font.Design = .monospaced
let usesUppercase = true
let headerTracking: CGFloat = 2
let usesGradientBackgrounds = false
let usesSoftShadows = false
func flatBackground(_ colorScheme: ColorScheme) -> Color {
colorScheme == .dark ? .black : Color(white: 0.95)
}
}
// MARK: - UIDesignStyle Extension
extension UIDesignStyle {
/// Returns the appropriate StyleProvider for this design style
var styleProvider: StyleProvider {
switch self {
case .brutalist:
return BrutalistStyle()
default:
return ClassicStyle()
}
}
}
Step 2: Verify the file compiles
Run: xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -20
Expected: BUILD SUCCEEDED
Step 3: Commit
git add SportsTime/Core/Design/StyleProvider.swift
git commit -m "$(cat <<'EOF'
feat(design): add StyleProvider protocol for style-specific constants
Introduces ClassicStyle and BrutalistStyle implementations for shared
component styling across the app.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
Task 2: Create TripDetailView Brutalist Variant
Files:
- Create:
SportsTime/Features/Trip/Views/Variants/Brutalist/TripDetailView_Brutalist.swift - Create:
SportsTime/Features/Trip/Views/Variants/Classic/TripDetailView_Classic.swift - Create:
SportsTime/Features/Trip/Views/AdaptiveTripDetailView.swift - Modify:
SportsTime/Features/Trip/Views/TripDetailView.swift(extract to Classic)
Step 1: Create directory structure
mkdir -p SportsTime/Features/Trip/Views/Variants/Classic
mkdir -p SportsTime/Features/Trip/Views/Variants/Brutalist
Step 2: Create AdaptiveTripDetailView router
Create SportsTime/Features/Trip/Views/AdaptiveTripDetailView.swift:
//
// AdaptiveTripDetailView.swift
// SportsTime
//
// Routes to the appropriate trip detail variant based on the selected design style.
//
import SwiftUI
struct AdaptiveTripDetailView: View {
let trip: Trip
let games: [String: RichGame]?
/// Initialize with trip and games dictionary
init(trip: Trip, games: [String: RichGame]) {
self.trip = trip
self.games = games
}
/// Initialize with just trip - games will be loaded from AppDataProvider
init(trip: Trip) {
self.trip = trip
self.games = nil
}
var body: some View {
switch DesignStyleManager.shared.currentStyle {
case .brutalist:
if let games = games {
TripDetailView_Brutalist(trip: trip, games: games)
} else {
TripDetailView_Brutalist(trip: trip)
}
default:
if let games = games {
TripDetailView_Classic(trip: trip, games: games)
} else {
TripDetailView_Classic(trip: trip)
}
}
}
}
Step 3: Copy existing TripDetailView to Classic variant
Copy TripDetailView.swift to Variants/Classic/TripDetailView_Classic.swift and rename the struct to TripDetailView_Classic.
Key changes:
- Rename
struct TripDetailView→struct TripDetailView_Classic - Rename
struct DaySection→struct DaySection_Classic - Rename
struct GameRow→struct GameRow_Classic - Rename
struct TravelSection→struct TravelSection_Classic - Rename
struct EVChargerRow→struct EVChargerRow_Classic - Keep
struct ShareSheetas-is (shared utility) - Keep
enum ItinerarySectionas-is (shared model) - Remove the #Preview at the bottom
Step 4: Create TripDetailView_Brutalist
Create SportsTime/Features/Trip/Views/Variants/Brutalist/TripDetailView_Brutalist.swift:
//
// TripDetailView_Brutalist.swift
// SportsTime
//
// BRUTALIST: Raw, unpolished, anti-design trip details.
// Monospace typography, harsh borders, ticket stub aesthetics.
//
import SwiftUI
import SwiftData
import MapKit
struct TripDetailView_Brutalist: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.colorScheme) private var colorScheme
let trip: Trip
private let providedGames: [String: RichGame]?
@Query private var savedTrips: [SavedTrip]
@State private var showProPaywall = false
@State private var showExportSheet = false
@State private var exportURL: URL?
@State private var isExporting = false
@State private var exportProgress: PDFAssetPrefetcher.PrefetchProgress?
@State private var mapCameraPosition: MapCameraPosition = .automatic
@State private var isSaved = false
@State private var routePolylines: [MKPolyline] = []
@State private var isLoadingRoutes = false
@State private var loadedGames: [String: RichGame] = [:]
@State private var isLoadingGames = false
private let exportService = ExportService()
private let dataProvider = AppDataProvider.shared
private var games: [String: RichGame] {
providedGames ?? loadedGames
}
private var bgColor: Color {
colorScheme == .dark ? .black : Color(white: 0.95)
}
private var textColor: Color {
colorScheme == .dark ? .white : .black
}
init(trip: Trip, games: [String: RichGame]) {
self.trip = trip
self.providedGames = games
}
init(trip: Trip) {
self.trip = trip
self.providedGames = nil
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
// HERO MAP - Sharp edges
heroMapSection
.frame(height: 280)
// HEADER
tripHeader
.padding(.horizontal, 16)
.padding(.top, 24)
perforatedDivider
.padding(.vertical, 24)
// STATS ROW
statsRow
.padding(.horizontal, 16)
perforatedDivider
.padding(.vertical, 24)
// SCORE CARD
if let score = trip.score {
scoreCard(score)
.padding(.horizontal, 16)
perforatedDivider
.padding(.vertical, 24)
}
// ITINERARY
itinerarySection
.padding(.horizontal, 16)
.padding(.bottom, 32)
}
}
.background(bgColor)
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
ShareButton(trip: trip, style: .icon)
.foregroundStyle(Theme.warmOrange)
Button {
if StoreManager.shared.isPro {
Task { await exportPDF() }
} else {
showProPaywall = true
}
} label: {
HStack(spacing: 2) {
Image(systemName: "doc.fill")
if !StoreManager.shared.isPro {
ProBadge()
}
}
.foregroundStyle(Theme.warmOrange)
}
}
}
.sheet(isPresented: $showExportSheet) {
if let url = exportURL {
ShareSheet(items: [url])
}
}
.sheet(isPresented: $showProPaywall) {
PaywallView()
}
.onAppear { checkIfSaved() }
.task { await loadGamesIfNeeded() }
.overlay {
if isExporting {
exportProgressOverlay
}
}
}
// MARK: - Perforated Divider
private var perforatedDivider: some View {
HStack(spacing: 8) {
ForEach(0..<25, id: \.self) { _ in
Circle()
.fill(textColor.opacity(0.3))
.frame(width: 6, height: 6)
}
}
.frame(maxWidth: .infinity)
.padding(.horizontal, 16)
}
// MARK: - Hero Map Section
private var heroMapSection: some View {
ZStack(alignment: .topTrailing) {
Map(position: $mapCameraPosition, interactionModes: []) {
ForEach(stopCoordinates.indices, id: \.self) { index in
let stop = stopCoordinates[index]
Annotation(stop.name, coordinate: stop.coordinate) {
Rectangle()
.fill(index == 0 ? Theme.warmOrange : Theme.routeGold)
.frame(width: 12, height: 12)
}
}
ForEach(routePolylines.indices, id: \.self) { index in
MapPolyline(routePolylines[index])
.stroke(Theme.routeGold, lineWidth: 3)
}
}
.mapStyle(.standard(elevation: .flat))
// Save button - sharp rectangle
Button { toggleSaved() } label: {
Image(systemName: isSaved ? "heart.fill" : "heart")
.font(.title3)
.foregroundStyle(isSaved ? .red : textColor)
.padding(12)
.background(bgColor)
.border(textColor.opacity(0.3), width: 1)
}
.padding(12)
}
.task {
updateMapRegion()
await fetchDrivingRoutes()
}
}
// MARK: - Header
private var tripHeader: some View {
VStack(alignment: .leading, spacing: 8) {
// Date range
Text(trip.formattedDateRange.uppercased())
.font(.system(.caption, design: .monospaced))
.foregroundStyle(textColor.opacity(0.5))
.tracking(2)
// Route as arrow chain
Text(trip.stops.map { $0.city.uppercased() }.joined(separator: " → "))
.font(.system(.title3, design: .monospaced).bold())
.foregroundStyle(textColor)
.lineLimit(2)
// Sport badges - bordered rectangles
HStack(spacing: 8) {
ForEach(Array(trip.uniqueSports), id: \.self) { sport in
HStack(spacing: 4) {
Image(systemName: sport.iconName)
.font(.caption2)
Text(sport.rawValue.uppercased())
.font(.system(.caption2, design: .monospaced))
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.foregroundStyle(sport.themeColor)
.border(sport.themeColor.opacity(0.5), width: 1)
}
}
}
}
// MARK: - Stats Row
private var statsRow: some View {
HStack(spacing: 12) {
brutalistStat(value: "\(trip.tripDuration)", label: "DAYS")
brutalistStat(value: "\(trip.stops.count)", label: "CITIES")
brutalistStat(value: "\(trip.totalGames)", label: "GAMES")
}
}
private func brutalistStat(value: String, label: String) -> some View {
VStack(spacing: 4) {
Text(value)
.font(.system(.title, design: .monospaced).bold())
.foregroundStyle(textColor)
Text("[ \(label) ]")
.font(.system(.caption2, design: .monospaced))
.foregroundStyle(textColor.opacity(0.5))
}
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.border(textColor.opacity(0.2), width: 1)
}
// MARK: - Score Card
private func scoreCard(_ score: TripScore) -> some View {
VStack(spacing: 16) {
HStack {
Text("[ TRIP SCORE ]")
.font(.system(.caption, design: .monospaced))
.foregroundStyle(textColor.opacity(0.5))
Spacer()
Text(score.scoreGrade)
.font(.system(size: 48, weight: .black, design: .monospaced))
.foregroundStyle(Theme.warmOrange)
}
HStack(spacing: 8) {
scoreItem(label: "GAMES", value: score.gameQualityScore)
scoreItem(label: "ROUTE", value: score.routeEfficiencyScore)
scoreItem(label: "BALANCE", value: score.leisureBalanceScore)
scoreItem(label: "PREFS", value: score.preferenceAlignmentScore)
}
}
.padding(16)
.border(textColor.opacity(0.2), width: 2)
}
private func scoreItem(label: String, value: Double) -> some View {
VStack(spacing: 4) {
Text(String(format: "%.0f", value))
.font(.system(.headline, design: .monospaced).bold())
.foregroundStyle(textColor)
Text(label)
.font(.system(.caption2, design: .monospaced))
.foregroundStyle(textColor.opacity(0.4))
}
.frame(maxWidth: .infinity)
}
// MARK: - Itinerary
private var itinerarySection: some View {
VStack(alignment: .leading, spacing: 16) {
Text("[ ITINERARY ]")
.font(.system(.caption, design: .monospaced))
.foregroundStyle(textColor.opacity(0.5))
if isLoadingGames {
Text("LOADING...")
.font(.system(.body, design: .monospaced))
.foregroundStyle(textColor.opacity(0.5))
.frame(maxWidth: .infinity)
.padding(.vertical, 32)
} else {
ForEach(Array(itinerarySections.enumerated()), id: \.offset) { index, section in
switch section {
case .day(let dayNumber, let date, let gamesOnDay):
DaySection_Brutalist(
dayNumber: dayNumber,
date: date,
games: gamesOnDay
)
case .travel(let segment):
TravelSection_Brutalist(segment: segment)
}
}
}
}
}
// MARK: - Export Progress Overlay
private var exportProgressOverlay: some View {
ZStack {
Color.black.opacity(0.8)
.ignoresSafeArea()
VStack(spacing: 16) {
Text("CREATING PDF")
.font(.system(.headline, design: .monospaced))
.foregroundStyle(.white)
Text(exportProgress?.currentStep.uppercased() ?? "PREPARING...")
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.white.opacity(0.7))
if let progress = exportProgress {
Text("\(Int(progress.percentComplete * 100))%")
.font(.system(.title, design: .monospaced).bold())
.foregroundStyle(Theme.warmOrange)
}
}
.padding(32)
.border(Color.white.opacity(0.3), width: 2)
}
}
// MARK: - Computed Properties (same as Classic)
private var stopCoordinates: [(name: String, coordinate: CLLocationCoordinate2D)] {
trip.stops.compactMap { stop -> (String, CLLocationCoordinate2D)? in
if let coord = stop.coordinate { return (stop.city, coord) }
if let stadiumId = stop.stadium,
let stadium = dataProvider.stadium(for: stadiumId) {
return (stadium.name, stadium.coordinate)
}
return nil
}
}
private var itinerarySections: [ItinerarySection] {
// Same logic as Classic variant
var sections: [ItinerarySection] = []
var dayCitySections: [(dayNumber: Int, date: Date, city: String, games: [RichGame])] = []
let days = tripDays
for (index, dayDate) in days.enumerated() {
let dayNum = index + 1
let gamesOnDay = gamesOn(date: dayDate)
guard !gamesOnDay.isEmpty else { continue }
var gamesByCity: [(city: String, games: [RichGame])] = []
for game in gamesOnDay {
let city = game.stadium.city
if let lastIndex = gamesByCity.indices.last, gamesByCity[lastIndex].city == city {
gamesByCity[lastIndex].games.append(game)
} else {
gamesByCity.append((city, [game]))
}
}
for cityGroup in gamesByCity {
dayCitySections.append((dayNum, dayDate, cityGroup.city, cityGroup.games))
}
}
for (index, section) in dayCitySections.enumerated() {
if index > 0 {
let prevCity = dayCitySections[index - 1].city
let currentCity = section.city
if !prevCity.isEmpty && !currentCity.isEmpty && prevCity != currentCity {
if let travelSegment = findTravelSegment(from: prevCity, to: currentCity) {
sections.append(.travel(travelSegment))
}
}
}
sections.append(.day(dayNumber: section.dayNumber, date: section.date, games: section.games))
}
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 }
let foundGames = allGameIds.compactMap { games[$0] }
return foundGames.filter { richGame in
calendar.startOfDay(for: richGame.game.dateTime) == dayStart
}.sorted { $0.game.dateTime < $1.game.dateTime }
}
private func findTravelSegment(from fromCity: String, to toCity: String) -> TravelSegment? {
let fromLower = fromCity.lowercased().trimmingCharacters(in: .whitespaces)
let toLower = toCity.lowercased().trimmingCharacters(in: .whitespaces)
return trip.travelSegments.first { segment in
let segmentFrom = segment.fromLocation.name.lowercased().trimmingCharacters(in: .whitespaces)
let segmentTo = segment.toLocation.name.lowercased().trimmingCharacters(in: .whitespaces)
return segmentFrom == fromLower && segmentTo == toLower
}
}
// MARK: - Actions (same as Classic)
private func loadGamesIfNeeded() async {
guard providedGames == nil else { return }
let gameIds = trip.stops.flatMap { $0.games }
guard !gameIds.isEmpty else { return }
isLoadingGames = true
var loaded: [String: RichGame] = [:]
for gameId in gameIds {
if let game = try? await dataProvider.fetchGame(by: gameId),
let richGame = dataProvider.richGame(from: game) {
loaded[gameId] = richGame
}
}
loadedGames = loaded
isLoadingGames = false
}
private func exportPDF() async {
isExporting = true
exportProgress = nil
do {
let url = try await exportService.exportToPDF(trip: trip, games: games) { progress in
await MainActor.run { self.exportProgress = progress }
}
exportURL = url
showExportSheet = true
} catch { }
isExporting = false
}
private func toggleSaved() {
if isSaved { unsaveTrip() } else { saveTrip() }
}
private func saveTrip() {
if !StoreManager.shared.isPro && savedTrips.count >= StoreManager.freeTripLimit {
showProPaywall = true
return
}
guard let savedTrip = SavedTrip.from(trip, games: games, status: .planned) else { return }
modelContext.insert(savedTrip)
if let _ = try? modelContext.save() {
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
isSaved = true
}
}
}
private func unsaveTrip() {
let tripId = trip.id
let descriptor = FetchDescriptor<SavedTrip>(predicate: #Predicate { $0.id == tripId })
if let savedTrips = try? modelContext.fetch(descriptor) {
for savedTrip in savedTrips { modelContext.delete(savedTrip) }
try? modelContext.save()
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
isSaved = false
}
}
}
private func checkIfSaved() {
let tripId = trip.id
let descriptor = FetchDescriptor<SavedTrip>(predicate: #Predicate { $0.id == tripId })
if let count = try? modelContext.fetchCount(descriptor), count > 0 {
isSaved = true
}
}
private func updateMapRegion() {
guard !stopCoordinates.isEmpty else { return }
let coordinates = stopCoordinates.map(\.coordinate)
let lats = coordinates.map(\.latitude)
let lons = coordinates.map(\.longitude)
guard let minLat = lats.min(), let maxLat = lats.max(),
let minLon = lons.min(), let maxLon = lons.max() else { return }
let center = CLLocationCoordinate2D(
latitude: (minLat + maxLat) / 2,
longitude: (minLon + maxLon) / 2
)
let latSpan = (maxLat - minLat) * 1.3 + 0.5
let lonSpan = (maxLon - minLon) * 1.3 + 0.5
mapCameraPosition = .region(MKCoordinateRegion(
center: center,
span: MKCoordinateSpan(latitudeDelta: max(latSpan, 1), longitudeDelta: max(lonSpan, 1))
))
}
private func fetchDrivingRoutes() async {
let stops = stopCoordinates
guard stops.count >= 2 else { return }
isLoadingRoutes = true
var polylines: [MKPolyline] = []
for i in 0..<(stops.count - 1) {
let source = stops[i]
let destination = stops[i + 1]
let request = MKDirections.Request()
let sourceLocation = CLLocation(latitude: source.coordinate.latitude, longitude: source.coordinate.longitude)
let destLocation = CLLocation(latitude: destination.coordinate.latitude, longitude: destination.coordinate.longitude)
request.source = MKMapItem(location: sourceLocation, address: nil)
request.destination = MKMapItem(location: destLocation, address: nil)
request.transportType = .automobile
let directions = MKDirections(request: request)
do {
let response = try await directions.calculate()
if let route = response.routes.first {
polylines.append(route.polyline)
}
} catch {
let straightLine = MKPolyline(coordinates: [source.coordinate, destination.coordinate], count: 2)
polylines.append(straightLine)
}
}
routePolylines = polylines
isLoadingRoutes = false
}
}
// MARK: - Day Section (Brutalist)
struct DaySection_Brutalist: View {
let dayNumber: Int
let date: Date
let games: [RichGame]
@Environment(\.colorScheme) private var colorScheme
private var textColor: Color {
colorScheme == .dark ? .white : .black
}
private var formattedDate: String {
date.formatted(.dateTime.weekday(.wide).month().day()).uppercased()
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Day header
HStack {
Text("DAY \(dayNumber)")
.font(.system(.title2, design: .monospaced).bold())
.foregroundStyle(textColor)
Spacer()
Text("\(games.count) GAME\(games.count > 1 ? "S" : "")")
.font(.system(.caption, design: .monospaced))
.foregroundStyle(Theme.warmOrange)
}
Text(formattedDate)
.font(.system(.caption, design: .monospaced))
.foregroundStyle(textColor.opacity(0.5))
.tracking(1)
// City
if let city = games.first?.stadium.city {
HStack(spacing: 4) {
Image(systemName: "mappin")
.font(.caption2)
Text(city.uppercased())
.font(.system(.caption, design: .monospaced))
}
.foregroundStyle(textColor.opacity(0.6))
}
// Games
ForEach(games, id: \.game.id) { richGame in
GameRow_Brutalist(game: richGame)
}
}
.padding(16)
.border(textColor.opacity(0.2), width: 1)
}
}
// MARK: - Game Row (Brutalist)
struct GameRow_Brutalist: View {
let game: RichGame
@Environment(\.colorScheme) private var colorScheme
private var textColor: Color {
colorScheme == .dark ? .white : .black
}
var body: some View {
HStack(spacing: 12) {
// Sport indicator - vertical bar
Rectangle()
.fill(game.game.sport.themeColor)
.frame(width: 4)
VStack(alignment: .leading, spacing: 4) {
// Matchup
HStack(spacing: 8) {
Text(game.awayTeam.abbreviation)
.font(.system(.body, design: .monospaced).bold())
Text("@")
.font(.system(.caption, design: .monospaced))
.foregroundStyle(textColor.opacity(0.5))
Text(game.homeTeam.abbreviation)
.font(.system(.body, design: .monospaced).bold())
}
.foregroundStyle(textColor)
// Stadium
Text(game.stadium.name.uppercased())
.font(.system(.caption2, design: .monospaced))
.foregroundStyle(textColor.opacity(0.5))
}
Spacer()
// Time
Text(game.localGameTimeShort)
.font(.system(.subheadline, design: .monospaced).bold())
.foregroundStyle(Theme.warmOrange)
}
.padding(12)
.background(textColor.opacity(0.03))
.border(textColor.opacity(0.1), width: 1)
}
}
// MARK: - Travel Section (Brutalist)
struct TravelSection_Brutalist: View {
let segment: TravelSegment
@Environment(\.colorScheme) private var colorScheme
private var textColor: Color {
colorScheme == .dark ? .white : .black
}
var body: some View {
VStack(spacing: 0) {
// Connector
Rectangle()
.fill(Theme.routeGold)
.frame(width: 2, height: 20)
// Travel card
HStack(spacing: 12) {
Image(systemName: "car.fill")
.foregroundStyle(Theme.routeGold)
VStack(alignment: .leading, spacing: 2) {
Text("[ TRAVEL ]")
.font(.system(.caption2, design: .monospaced))
.foregroundStyle(textColor.opacity(0.4))
Text("\(segment.fromLocation.name.uppercased()) → \(segment.toLocation.name.uppercased())")
.font(.system(.caption, design: .monospaced))
.foregroundStyle(textColor)
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text(segment.formattedDistance.uppercased())
.font(.system(.caption, design: .monospaced).bold())
Text(segment.formattedDuration.uppercased())
.font(.system(.caption2, design: .monospaced))
.foregroundStyle(textColor.opacity(0.5))
}
.foregroundStyle(textColor)
}
.padding(12)
.border(Theme.routeGold.opacity(0.3), width: 1)
// Connector
Rectangle()
.fill(Theme.routeGold)
.frame(width: 2, height: 20)
}
}
}
Step 5: Update original TripDetailView to be a typealias
Replace TripDetailView.swift with a simple redirect:
//
// TripDetailView.swift
// SportsTime
//
// Redirects to AdaptiveTripDetailView for backward compatibility.
//
import SwiftUI
typealias TripDetailView = AdaptiveTripDetailView
Step 6: Verify the build
Run: xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -30
Expected: BUILD SUCCEEDED
Step 7: Commit
git add SportsTime/Features/Trip/Views/
git commit -m "$(cat <<'EOF'
feat(trip): add brutalist variant for TripDetailView
- Create AdaptiveTripDetailView router
- Add TripDetailView_Classic (extracted from original)
- Add TripDetailView_Brutalist with monospace fonts, borders, perforated dividers
- Update TripDetailView as typealias for backward compatibility
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
Task 3: Create SavedTripsListView Variants
Files:
- Create:
SportsTime/Features/Home/Views/Variants/Classic/SavedTripsListView_Classic.swift - Create:
SportsTime/Features/Home/Views/Variants/Brutalist/SavedTripsListView_Brutalist.swift - Create:
SportsTime/Features/Home/Views/AdaptiveSavedTripsListView.swift - Modify:
SportsTime/Features/Home/Views/HomeView.swift(extract SavedTripsListView, update to use Adaptive)
Step 1: Create directory structure
mkdir -p SportsTime/Features/Home/Views/Variants/Classic
mkdir -p SportsTime/Features/Home/Views/Variants/Brutalist
Step 2: Extract SavedTripsListView to Classic variant
Move SavedTripsListView (lines 406-581 from HomeView.swift) to Variants/Classic/SavedTripsListView_Classic.swift, renaming to SavedTripsListView_Classic.
Step 3: Create SavedTripsListView_Brutalist
Create SportsTime/Features/Home/Views/Variants/Brutalist/SavedTripsListView_Brutalist.swift with brutalist styling.
Step 4: Create AdaptiveSavedTripsListView router
Step 5: Update HomeView.swift
Replace inline SavedTripsListView with AdaptiveSavedTripsListView.
Step 6: Verify build and commit
Task 4: Create ScheduleListView Variants
Files:
- Create:
SportsTime/Features/Schedule/Views/Variants/Classic/ScheduleListView_Classic.swift - Create:
SportsTime/Features/Schedule/Views/Variants/Brutalist/ScheduleListView_Brutalist.swift - Create:
SportsTime/Features/Schedule/Views/AdaptiveScheduleListView.swift - Modify:
SportsTime/Features/Schedule/Views/ScheduleListView.swift(typealias) - Modify:
SportsTime/Features/Home/Views/HomeView.swift(update Schedule tab)
Step 1: Create directory structure
mkdir -p SportsTime/Features/Schedule/Views/Variants/Classic
mkdir -p SportsTime/Features/Schedule/Views/Variants/Brutalist
Step 2-6: Same pattern as TripDetailView
Task 5: Create SettingsView Variants
Files:
- Create:
SportsTime/Features/Settings/Views/Variants/Classic/SettingsView_Classic.swift - Create:
SportsTime/Features/Settings/Views/Variants/Brutalist/SettingsView_Brutalist.swift - Create:
SportsTime/Features/Settings/Views/AdaptiveSettingsView.swift - Modify:
SportsTime/Features/Settings/Views/SettingsView.swift(typealias) - Modify:
SportsTime/Features/Home/Views/HomeView.swift(update Settings tab)
Step 1: Create directory structure
mkdir -p SportsTime/Features/Settings/Views/Variants/Classic
mkdir -p SportsTime/Features/Settings/Views/Variants/Brutalist
Step 2-6: Same pattern as TripDetailView
Task 6: Run Tests and Fix Issues
Step 1: Run full test suite
Run: xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test
Expected: All tests pass
Step 2: Fix any failing tests
Update test files that reference view types directly.
Step 3: Commit fixes
Task 7: Final Verification and Documentation Update
Step 1: Test all design styles in simulator
- Build and run app
- Go to Settings → Home Screen Style
- Select "Classic" - verify all screens look correct
- Select "Brutalist" - verify all screens have brutalist styling
Step 2: Update CLAUDE.md if needed
Add note about adaptive view pattern if helpful for future development.
Step 3: Final commit
git add -A
git commit -m "$(cat <<'EOF'
docs: complete brutalist app-wide implementation
All main screens now support Classic and Brutalist design variants:
- TripDetailView
- SavedTripsListView
- ScheduleListView
- SettingsView
Other 22 design styles fall back to Classic for non-home screens.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"