LocationSearchSheet now shows only stadium cities (with sport badges) when
selecting start/end locations, preventing users from picking cities with no
stadiums. TripWizardViewModel filters available sports to the union of sports
at the selected cities, and clears invalid selections when locations change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change attraction category color from .yellow to orange variant for
readable text in both light and dark mode (QuickAddItemSheet, POIDetailSheet)
- Fix "optional" badge to use textPrimary-based colors for strong contrast
- Bump paywall dashed separator from 0.4 to 0.6 opacity
- Fix SuggestedTripCard bullet separator from textMuted(0.5) to textSecondary
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move .sheet(isPresented: $showPaywall) from subscription section to
top-level body so StoreManager state changes don't dismiss the sheet.
Reposition heart (16pt top/trailing) and map (16pt bottom/trailing,
above gradient) buttons on trip detail map.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds .contentShape(Rectangle()) or .contentShape(Capsule()) to 11 buttons,
NavigationLinks, and onTapGesture handlers across 8 files where only the
visible content (text/icons) was receiving taps instead of the full row.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gate Icon Generator section behind #if DEBUG and group it with other debug
sections at the bottom of Settings. Remove auto-focus on description field,
dismiss keyboard on return key and on scroll.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Expand POI categories from 5 to 7 (restaurant, bar, coffee, hotel, parking, attraction, entertainment)
- Add category filter chips with per-category API calls and caching
- Add delete button with confirmation dialog to Edit Item sheet
- Fix itinerary items not persisting: use LocalItineraryItem (SwiftData) as primary store with CloudKit sync as secondary, register model in schema
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update games_canonical.json to use ISO 8601 UTC timestamps (game_datetime_utc)
- Fix BootstrapService timezone-aware parsing for venue-local fallback
- Fix thread-unsafe shared DateFormatter in RichGame local time display
- Bump SchemaVersion to 4 to force re-bootstrap with correct UTC data
- Restructure schedule view: group by date instead of sport, with sport
icons on each row and date section headers showing game counts
- Fix schedule row backgrounds using Theme.cardBackground instead of black
- Sort games by UTC time with local-time tiebreaker for same-instant games
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add mapItem field to POISearchService.POI for Apple Maps integration
- Merge description + location into single combined card in QuickAddItemSheet
- Auto-load nearby POIs when regionCoordinate is available, with detail sheet
- Create POIDetailSheet with map preview, metadata, and one-tap add-to-day
- Add poiAddedToDay/poiDetailViewed analytics events
- Add initial state to PlaceSearchSheet with search suggestions and flow layout
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Second audit round addressing data races, task stacking, unbounded
caches, and VoiceOver gaps across 7 files.
Concurrency:
- Move NSItemProvider @State access into MainActor block (3 drop handlers)
- Cancel stale ScheduleViewModel tasks on rapid filter changes
Memory:
- Replace unbounded image dict with LRUCache(countLimit: 50)
- Replace demo-mode asyncAfter with cancellable Task
Performance:
- Wrap debug NBA print() in #if DEBUG
- Cache visitsById via @State + onChange instead of per-render computed
- Pre-compute sportProgressFractions in ProgressViewModel
- Replace allGames computed property with hasGames bool check
- Cache sortedTrips via @State + onChange in SavedTripsListView
Accessibility:
- Add combined VoiceOver label to progress ring
- Combine away/home team text into single readable phrase
- Hide decorative StadiumDetailSheet icon from VoiceOver
- Add explicit accessibilityLabel to SportFilterChip
- Add combined accessibilityLabel to GameRowView
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical:
- ProgressViewModel: use single stored ModelContext instead of creating
new ones per operation (deleteVisit silently no-op'd)
- ProgressViewModel: convert expensive computed properties to stored
with explicit recompute after mutations (3x recomputation per render)
Memory:
- AnimatedSportsIcon: replace recursive GCD asyncAfter with Task loop,
cancelled in onDisappear (19 unkillable timer chains)
- ItineraryItemService: remove [weak self] from actor Task (semantically
wrong, silently drops flushPendingUpdates)
- VisitPhotoService: remove [weak self] from @MainActor Task closures
Concurrency:
- StoreManager: replace nested MainActor.run{Task{}} with direct await
in listenForTransactions (fire-and-forget race)
- VisitPhotoService: move JPEG encoding/file writing off MainActor via
nonisolated static helper + Task.detached
- SportsIconImageGenerator: replace GCD dispatch with Task.detached for
structured concurrency compliance
Performance:
- Game/RichGame: cache DateFormatters as static lets instead of
allocating per-call (hundreds of allocations in schedule view)
- TripDetailView: wrap ~10 routeWaypoints print() in #if DEBUG, remove
2 let _ = print() from TripMapView.body (fires every render)
Accessibility:
- GameRow: add combined VoiceOver label (was reading abbreviations
letter-by-letter)
- Sport badges: add accessibilityLabel to prevent SF symbol name readout
- SportsTimeApp: post UIAccessibility.screenChanged after bootstrap
completes so VoiceOver users know app is ready
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add a11y label to ProgressMapView reset button and progress bar values
- Fix CADisplayLink retain cycle in ItineraryTableViewController via deinit
- Add [weak self] to PhotoGalleryViewModel Task closure
- Add @MainActor to TripWizardViewModel, remove manual MainActor.run hop
- Fix O(n²) rank lookup in PollDetailView/DebugPollPreviewView with enumerated()
- Cache itinerarySections via ItinerarySectionBuilder static extraction + @State
- Convert CanonicalSyncService/BootstrapService from actor to @MainActor final class
- Add .accessibilityHidden(true) to RegionMapSelector Map to prevent duplicate VoiceOver
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 22 new UI tests across 8 test files covering Home, Schedule, Progress,
Settings, TabNavigation, TripSaving, and TripOptions. Add accessibility
identifiers to 11 view files for test element discovery. Fix sport chip
assertion logic (all sports start selected, tap deselects), scroll container
issues on iOS 26 nested ScrollViews, toggle interaction, and delete trip flow.
Update QA coverage map from 32 to 54 automated test cases.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add debug-only Marketing Video Mode toggle that enables hands-free
screen recording across the app: auto-scrolling Featured Trips carousel,
auto-filling trip wizard, smooth trip detail scrolling via CADisplayLink,
and trip options auto-sort with scroll.
Add Remotion marketing video project with 6 scene compositions using
image sequences extracted from screen recordings, varied phone entrance
animations, and deduped frames for smooth playback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Accessibility buttons split the map into equal-width thirds, intercepting
taps before the coordinate-based logic. Tapping the visual West region
could hit the Central button. Adding .allowsHitTesting(false) lets taps
pass through to MapReader's coordinate detection; VoiceOver still works.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add TravelInfo initializers and city normalization helpers to fix repeat
city-pair disambiguation. Improve drag-and-drop reordering with segment
index tracking and source-row-aware zone calculation. Enhance all five
scenario planners with better next-day departure handling and travel
segment placement. Add comprehensive tests across all planners.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace all system colors (.secondary, Color(.secondarySystemBackground),
etc.) with Theme.textPrimary/textSecondary/textMuted/cardBackground/
surfaceGlow across 13 views. Remove PostHog debug logging. Add debug
settings for sample trips and hardcoded group poll preview.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Include segment index in travel anchor IDs ("travel:INDEX:from->to")
so Follow Team trips visiting the same city pair multiple times get
unique, independently addressable travel segments. Prevents override
dictionary collisions and incorrect validDayRange lookups.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Travel segments appeared one day too late on featured/suggested trips
when stops had next-morning departures. The placement formula double-
counted by using fromDayNum+1 as both minDay and defaultDay, then the
invalid-range fallback used minDay (the overshooting value) instead of
the arrival day. Also replaced TripDetailView's inline copy of the
placement logic with TravelPlacement.computeTravelByDay() so the UI
uses the same tested algorithm.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add VoiceOver labels, hints, and element grouping across all 60+ views
- Add Reduce Motion support (Theme.Animation.prefersReducedMotion) to all animations
- Replace fixed font sizes with semantic Dynamic Type styles
- Hide decorative elements from VoiceOver with .accessibilityHidden(true)
- Add .minimumHitTarget() modifier ensuring 44pt touch targets
- Add AccessibilityAnnouncer utility for VoiceOver announcements
- Improve color contrast values in Theme.swift for WCAG AA compliance
- Extract CloudKitContainerConfig for explicit container identity
- Remove PostHog debug console log from AnalyticsManager
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix repeat-city travel placement: use stop indices instead of global city name
matching so Follow Team trips with repeat cities show travel correctly
- Add TravelPlacement helper and regression tests (7 tests)
- Add alternate app icons for each theme, auto-switch on theme change
- Fix index-out-of-range crash in AnimatedSportsBackground (19 configs, was iterating 20)
- Add marketing video configs, engine, and new video components
- Add docs and data exports
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove Scripts/ directory (scraper no longer needed)
- Add themed background documentation to CLAUDE.md
- Add .gitignore for marketing-videos to prevent node_modules tracking
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two bugs fixed in "By Games" trip planning mode:
1. Calendar navigation: DateRangePicker now navigates to the selected
game's month when startDate changes externally, instead of staying
on the current month.
2. Date range calculation: Fixed race condition where date range was
calculated before games were loaded. Now updateDateRangeForSelectedGames()
is called after loadSummaryGames() completes.
3. Bonus games: planTrip() now uses the UI-selected 7-day date range
instead of overriding it with just the anchor game dates. This allows
ScenarioBPlanner to find additional games within the trip window.
Added regression tests to verify gameFirst mode includes bonus games.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Game times were incorrectly using device timezone in several views.
Now all game time displays use the stadium's local timezone via
RichGame.localGameTime/localGameTimeShort properties.
Fixes:
- PDFGenerator: was using game.gameDate (midnight UTC) instead of actual time
- GameRowCompact: was formatting with device timezone
- TimelineGameRow: was using .formatted() with device timezone
Also adds Date+GameTime.swift extension for centralized timezone formatting.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create shared TipsSection component for displaying planning tips
- Add TipsSection to all 22 home content variants
- Fix displayedTips population with onAppear in HomeView
- Add map buttons to GameRowCompact (opens stadium in Apple Maps)
- Add map buttons to TravelRowView (opens driving directions)
- Add map buttons to CustomItemRowView (opens location when GPS available)
- Add AppleMapsLauncher.openLocation() and openDirections() methods
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixed PDF export missing the last day when games occur on departure date.
Root cause: Trip.itineraryDays() calculated lastActivityDate as departure - 1,
assuming departure is always after the last activity. When games happen ON the
departure date, that day was skipped.
Fix: Check if the last stop has games. If so, include the departure date in
the itinerary loop.
Also includes:
- buildCompleteItineraryItems() to merge games, travel, and custom items
- Magazine-style PDF layout with sport-specific accent colors
- Proper iteration over all trip days in PDF generator
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add floating action button (bottom-right) on trip map
- Create AppleMapsLauncher service to handle route opening
- Collect all routable stops (trip stops + custom items with coords)
- Handle >16 waypoints with alert offering to open in parts
- Silently skip stops without coordinates
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace confusing dual-mode AddItemSheet with streamlined QuickAddItemSheet:
- Single flow with optional location (vs Search/Custom toggle)
- Card-based layout with shadows, borders, and theme colors
- Enhanced CategoryPicker with emoji circles and press animations
- Separate PlaceSearchSheet for focused location search
- Improved header button with capsule style and accessibility labels
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Travel constraint validation was not working because ItineraryConstraints
had no game items to validate against - games came from RichGame objects
but were never converted to ItineraryItem for constraint checking.
Changes:
- Add city parameter to ItemKind.game enum case
- Create game ItineraryItems from RichGame data in buildItineraryData()
- Update isValidTravelPosition to compare against actual game sortOrders
- Fix tests to use appropriate game sortOrder conventions
Now travel is properly constrained to appear before arrival city games
and after departure city games.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract all itinerary reordering logic from ItineraryTableViewController into
ItineraryReorderingLogic.swift for testability. Key changes:
- Add flattenDays, dayNumber, travelRow, simulateMove pure functions
- Add calculateSortOrder with proper region classification (before/after games)
- Add computeValidDestinationRowsProposed with simulation+validation pattern
- Add coordinate space conversion helpers (proposedToOriginal, originalToProposed)
- Fix DragZones coordinate space mismatch (was mixing proposed/original indices)
- Add comprehensive documentation of coordinate space conventions
Test coverage includes:
- Row flattening order and semantic travel model
- Sort order calculation for before/after games regions
- Travel constraints validation
- DragZones coordinate space correctness
- Coordinate conversion helpers
- Edge cases (empty days, multi-day trips)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add constraint validation during drag using ItineraryConstraints
- Calculate invalid zones and barrier games when drag starts
- Apply visual dimming (alpha 0.3) to invalid drop zones during drag
- Highlight barrier games with gold border when dragging travel segments
- Block invalid drops using ItineraryConstraints.isValidPosition validation
- Add haptic feedback for drag interactions:
- Medium impact on pickup
- Light impact when entering valid zone
- Warning notification when entering invalid zone
- Soft impact on drop
The drag state is tracked via draggingItem, invalidRowIndices, and
barrierGameIds properties. Visual feedback is applied and removed
via applyDragVisualFeedback/removeDragVisualFeedback methods.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update CKModels.swift to remove deleted type references
- Migrate LocalCustomItem to LocalItineraryItem in SavedTrip.swift
- Update AppDelegate to handle subscription removal
- Refactor AddItemSheet to create ItineraryItem with CustomInfo
- Update ItineraryTableViewController and Wrapper for new model
- Refactor TripDetailView state, methods and callbacks
- Fix TripMapView to display custom items with new model structure
This completes the migration from the legacy CustomItineraryItem/TravelDayOverride
model to the unified ItineraryItem model with ItemKind enum.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add isEmpty parameter to show "No items yet, tap + to add" text
- Change date format to single line: "Day 1 - Friday, January 17"
- Update preview to test both empty and non-empty states
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add specialized row components for the new unified itinerary system:
- DayHeaderRow: Day number, date display, and add item button
- GameItemRow: Prominent card with sport color bar for games
- TravelItemRow: Gold-styled travel segments with drag handle
- CustomItemRow: Minimal custom items with icon, title, and optional time
All views follow existing Theme patterns and use SportColorBar for consistency.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Removed:
- CustomItineraryItem
- TravelDayOverride
- CustomItemService
- CustomItemSubscriptionService
- TravelOverrideService
- CustomItemRow
These are replaced by unified ItineraryItem model and service.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Merge Add button into DaySectionHeaderView to prevent items from being
dragged between the day header and Add button. The Add button now uses
a SwiftUI Button with its own tap handler instead of row selection.
Changes:
- Remove .addButton case from ItineraryRowItem enum
- Update DaySectionHeaderView to include Add button on the right
- Pass onAddTapped callback through configureDayHeaderCell
- Remove AddButtonRowView (no longer needed)
- Update documentation to reflect new row structure
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move Add button to appear immediately after Day header (before games)
- Split games out of dayHeader into separate row for correct ordering
- Add 600+ lines of inline documentation to ItineraryTableViewController
- Document architecture decisions, data flow, constraints, and algorithms
- Add function-level comments explaining drag/drop, sortOrder calculation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fixed map header not updating in ItineraryTableViewWrapper using Coordinator pattern
- Added routeVersion UUID to force Map re-render when routes change
- Fixed determineAnchor to scan backwards for correct anchor context
- Added location support to CustomItineraryItem (lat/lng/address)
- Added MapKit place search to AddItemSheet
- Added extensive debug logging for route waypoint calculation
Known issues:
- Custom items still not routing correctly after drag/drop
- Anchor type determination may still have bugs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace SwiftUI drag-drop with native UITableViewController for fluid reordering
- Add ItineraryTableViewController with native cell reordering and validation
- Add ItineraryTableViewWrapper for SwiftUI integration with header support
- Fix infinite layout loop by tracking header adjustment state
- Map and stats now scroll as table header with itinerary content
- Travel segments constrained to valid day ranges during drag
- One Add button per day (after game > after travel > rest day)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>