From eecc0f5f99ef3702215413571ead8267ff528e4f Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Fri, 16 Oct 2026 07:06:07 -0700 Subject: [PATCH 01/18] Integrate adaptive Where destinations and Duo lab --- Project.swift | 6 +- Where/WhereUI/AGENTS.md | 3 + Where/WhereUI/README.md | 36 ++ .../AdaptiveHistoryLabViewSnapshotTests.swift | 10 + .../LocationsViewSnapshotTests.swift | 20 +- .../SnapshotTests/YearViewSnapshotTests.swift | 20 +- .../adaptiveHistoryLab.outerPortrait_iPad.png | 3 + .../adaptiveHistoryLab.tabletop_iPad.png | 3 + ...darContent.HistoryColumn_historyColumn.png | 3 + ...e-empty-innerPortrait_duoInnerPortrait.png | 3 + ...aptive-empty-tabletop_duoInnerPortrait.png | 3 + ...-failed-innerPortrait_duoInnerPortrait.png | 3 + ...ptive-failed-tabletop_duoInnerPortrait.png | 3 + ...loading-innerPortrait_duoInnerPortrait.png | 3 + ...tive-loading-tabletop_duoInnerPortrait.png | 3 + .../locations.BrowseColumn_browseColumn.png | 3 + ...ations-book-Selected_duoInnerLandscape.png | 3 + ...ptive-Locations-book_duoInnerLandscape.png | 3 + ...tions-innerLandscape_duoInnerLandscape.png | 3 + ...nnerPortrait-Selected_duoInnerPortrait.png | 3 + ...cations-innerPortrait_duoInnerPortrait.png | 3 + ...ortrait_duoInnerPortrait_accessibility.png | 3 + ...ons-innerPortrait_duoInnerPortrait_ax5.png | 3 + ...nnerPortrait_duoInnerPortrait_contrast.png | 3 + ...ns-innerPortrait_duoInnerPortrait_dark.png | 3 + ...ons-innerPortrait_duoInnerPortrait_rtl.png | 3 + ...Adaptive-Locations-narrow_narrowWindow.png | 3 + ...tions-outerLandscape_duoOuterLandscape.png | 3 + ...cations-outerPortrait_duoOuterPortrait.png | 3 + ...ons-tabletop-Selected_duoInnerPortrait.png | 3 + ...ve-Locations-tabletop_duoInnerPortrait.png | 3 + ...aptive-Settings-book_duoInnerLandscape.png | 3 + ...tings-innerLandscape_duoInnerLandscape.png | 3 + ...nnerPortrait-Selected_duoInnerPortrait.png | 3 + ...ettings-innerPortrait_duoInnerPortrait.png | 3 + ...ortrait_duoInnerPortrait_accessibility.png | 3 + ...ngs-innerPortrait_duoInnerPortrait_ax5.png | 3 + ...nnerPortrait_duoInnerPortrait_contrast.png | 3 + ...gs-innerPortrait_duoInnerPortrait_dark.png | 3 + ...ngs-innerPortrait_duoInnerPortrait_rtl.png | 3 + ....Adaptive-Settings-narrow_narrowWindow.png | 3 + ...tings-outerLandscape_duoOuterLandscape.png | 3 + ...ettings-outerPortrait_duoOuterPortrait.png | 3 + ...ive-Settings-tabletop_duoInnerPortrait.png | 3 + ...e-Year-book-Selected_duoInnerLandscape.png | 3 + ...s.Adaptive-Year-book_duoInnerLandscape.png | 3 + ...-Year-innerLandscape_duoInnerLandscape.png | 3 + ...nnerPortrait-Selected_duoInnerPortrait.png | 3 + ...ve-Year-innerPortrait_duoInnerPortrait.png | 3 + ...ortrait_duoInnerPortrait_accessibility.png | 3 + ...ear-innerPortrait_duoInnerPortrait_ax5.png | 3 + ...nnerPortrait_duoInnerPortrait_contrast.png | 3 + ...ar-innerPortrait_duoInnerPortrait_dark.png | 3 + ...ear-innerPortrait_duoInnerPortrait_rtl.png | 3 + ...Tabs.Adaptive-Year-narrow_narrowWindow.png | 3 + ...-Year-outerLandscape_duoOuterLandscape.png | 3 + ...ve-Year-outerPortrait_duoOuterPortrait.png | 3 + ...e-Year-tabletop-Month_duoInnerPortrait.png | 3 + ...ear-tabletop-Selected_duoInnerPortrait.png | 3 + ...daptive-Year-tabletop_duoInnerPortrait.png | 3 + ...enceTimeline.BrowseColumn_browseColumn.png | 3 + ...eline.PlannedStay_iPhone_accessibility.png | 4 +- ...Timeline.WithData_iPhone_accessibility.png | 4 +- .../settings.BrowseColumn_browseColumn.png | 3 + ...e-empty-innerPortrait_duoInnerPortrait.png | 3 + ...aptive-empty-tabletop_duoInnerPortrait.png | 3 + ...-failed-innerPortrait_duoInnerPortrait.png | 3 + ...ptive-failed-tabletop_duoInnerPortrait.png | 3 + ...loading-innerPortrait_duoInnerPortrait.png | 3 + ...tive-loading-tabletop_duoInnerPortrait.png | 3 + .../Developer/DeveloperDestination.swift | 1 + .../Sources/Developer/DeveloperOverlay.swift | 83 +++-- .../Developer/DeveloperTabBarInset.swift | 121 +++++-- .../Sources/Developer/DeveloperTool.swift | 4 + .../Sources/Developer/DeveloperToolView.swift | 84 +++-- .../DuoLayout/AdaptiveHistoryLabModel.swift | 39 +++ .../DuoLayout/AdaptiveHistoryLabView.swift | 124 +++++++ .../AdaptiveHistorySnapshotFixture.swift | 127 +++++++ .../Flyover/WhereFlyoverCatalog.swift | 1 + .../Developer/Flyover/WhereFlyoverHost.swift | 1 + Where/WhereUI/Sources/MainTabs.swift | 148 +++++++- .../Sources/Primary/CalendarContentView.swift | 255 ++++++++++---- .../Primary/HistoryMonthControls.swift | 76 ++++ .../Primary/LocationWelcomeOverlay.swift | 10 +- .../Sources/Primary/LocationsCardsView.swift | 42 ++- .../Primary/LocationsFlightNotice.swift | 24 +- .../Sources/Primary/LocationsStateView.swift | 4 +- .../Sources/Primary/LocationsView.swift | 330 +++++++++++++++--- .../Primary/PresenceTimelineList.swift | 235 +++++++++---- .../Sources/Primary/RegionWelcomeCard.swift | 1 + Where/WhereUI/Sources/RootView.swift | 2 +- .../Sources/Settings/SettingsContent.swift | 255 ++++++++++++++ .../Sources/Settings/SettingsView.swift | 322 ++++++----------- Where/WhereUI/Sources/Year/YearView.swift | 101 +++++- .../Tests/CalendarContentViewTests.swift | 108 ++++++ .../Tests/DeveloperDestinationTests.swift | 1 + .../Tests/DeveloperTabBarInsetTests.swift | 44 ++- .../Tests/LocationForecastModelTests.swift | 12 +- Where/WhereUI/Tests/LocationsViewTests.swift | 108 ++++++ Where/WhereUI/Tests/MainTabsTests.swift | 67 ++++ .../Tests/PlannedStayEditorTests.swift | 59 ++++ .../Tests/PresenceTimelineListTests.swift | 50 +++ .../Support/AdaptiveHistoryLoadFixture.swift | 50 +++ Where/WhereUI/Tests/Support/TestStore.swift | 37 +- 104 files changed, 2628 insertions(+), 587 deletions(-) create mode 100644 Where/WhereUI/SnapshotTests/AdaptiveHistoryLabViewSnapshotTests.swift create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.HistoryColumn_historyColumn.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.BrowseColumn_browseColumn.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_accessibility.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-narrow_narrowWindow.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-outerLandscape_duoOuterLandscape.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-outerPortrait_duoOuterPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-book_duoInnerLandscape.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerLandscape_duoInnerLandscape.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait-Selected_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_accessibility.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_dark.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-narrow_narrowWindow.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-outerLandscape_duoOuterLandscape.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-outerPortrait_duoOuterPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-tabletop_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-narrow_narrowWindow.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerPortrait_duoOuterPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.BrowseColumn_browseColumn.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.BrowseColumn_browseColumn.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png create mode 100644 Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabModel.swift create mode 100644 Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift create mode 100644 Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistorySnapshotFixture.swift create mode 100644 Where/WhereUI/Sources/Primary/HistoryMonthControls.swift create mode 100644 Where/WhereUI/Sources/Settings/SettingsContent.swift create mode 100644 Where/WhereUI/Tests/PlannedStayEditorTests.swift create mode 100644 Where/WhereUI/Tests/PresenceTimelineListTests.swift create mode 100644 Where/WhereUI/Tests/Support/AdaptiveHistoryLoadFixture.swift diff --git a/Project.swift b/Project.swift index b9a41dfff..4bb1a1ce0 100644 --- a/Project.swift +++ b/Project.swift @@ -770,7 +770,11 @@ let project = Project( name: "WhereUISnapshotTests", bundleIdSuffix: "whereui.snapshot", productDependency: "WhereUI", - sources: ["Where/WhereUI/SnapshotTests/**"], + sources: [ + "Where/WhereUI/SnapshotTests/**", + "Where/WhereUI/Tests/Support/AdaptiveHistoryLoadFixture.swift", + "Where/WhereUI/Tests/Support/TestStore.swift", + ], extraPackageProducts: ["SnapshotKitTesting"], environmentVariables: snapshotEnvironment, ), diff --git a/Where/WhereUI/AGENTS.md b/Where/WhereUI/AGENTS.md index 0d2c31daa..780765c6e 100644 --- a/Where/WhereUI/AGENTS.md +++ b/Where/WhereUI/AGENTS.md @@ -296,6 +296,9 @@ Hosted tests that traverse SwiftUI accessibility must link and import `AccessibilitySnapshotParser` so a fresh simulator initializes its accessibility runtime. Guard: `CalendarContentViewTests`. +`Tests/Support/TestStore.swift` and `AdaptiveHistoryLoadFixture.swift` are shared by the unit and image bundles through `Project.swift`. +Keep support fixtures outside `SnapshotTests/`, whose Swift files each declare one matching suite. Use the store's read gates and failures for adaptive loading snapshots. Keep fixture setup and completion independent for each configuration. + `WhereStylesheetTests` pins every token default and trait-aware derivation. `WhereStylesheetEnvironmentTests` covers the `@Environment(\.stylesheet)` glue and `whereBroadwayRoot()` seeding, including the WhereWidgets path. diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index 8f08ec61b..8e45eb2d0 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -69,6 +69,42 @@ An automatic check that can reuse Core's cache preserves an active manual check Demo mode hides this surface and performs no update checks. +`MainTabs` owns one report model and one `SceneNavigationModel` for the session. +The navigation model retains tab selection, region routes, the compact year mode, Settings routes, search, and calendar anchors. +Each destination receives its own child state. Calendar, timeline, and Locations anchors survive pane remounts. +A report-year change waits for matching data before positioning. Stale visibility callbacks cannot replace the new year's anchor. +Calendar sections and day rows measure eagerly so offscreen estimates cannot move a restored month. +The timeline keeps its pinned overview outside the scrolling rows; large text places it in the content flow. +Timeline stays also measure eagerly so resizing cannot shift the content origin. +Timeline anchors refer to displayed stay segments or flight identities. Month +selection reveals the first overlapping displayed entry. + +On a wide window, Locations shows cards beside the selected region's calendar. +Without a region selection, the detail pane shows the year calendar. +Your Year shows the calendar and timeline together. Settings uses native list and detail navigation. +Narrow windows and accessibility text sizes use one pane. The compact calendar/timeline choice remains stored while both panes are visible. +Theme choices stack when the available pane cannot fit two readable card specimens. + +`AdaptiveHistoryGeometry` reads independent edge insets and the system's reserved regions. +`AdaptiveHistoryArrangement` combines that geometry with the stylesheet's pane limits. +An active horizontal division places the selected month above the hinge and browsing controls below it. +A vertical division retains native browse/detail navigation. Native containers own system-bar placement. +The adapter uses container geometry, without device-name or hinge-angle checks. + +`HistoryNavigationModel` links explicit month and stay selections through typed day intervals. +It highlights the interval and reveals it in the companion pane. Scroll observations only retain local anchors. +Each month reveal has a typed request identity. Consuming it preserves that identity, so repeated selections work without an observer feedback loop. +Planning sheets sit above the changing containers, with one shared presentation owner per destination. +The developer overlay and welcome overlay use the same geometry adapter to find clear space. + +The DEBUG **Duo Layout Lab** retains one production demo world while its pose control changes bounds, traits, and synthetic regions. +Embedded demo tools omit the session's Exit Demo action. The containing tool owns dismissal. +Its measurements come from the pinned runtime; see [Duo measurements](../../Shared/SnapshotKit/Documentation/DuoMeasurements.json). +Viewport shell captures supplement full-content captures of the shared scrolling panes. +Shell fixtures request their initial browse position after asynchronous demo preparation. +Synthetic layouts do not validate system bars, camera activation, or physical folding. Those require native-device checks. +The reusable panes also support iPad layouts. There is no HomeView target. + - **`RootView`** — the app root: the typed launch plan (via [`LifecycleKit`](../../Shared/LifecycleKit), rendered by [`LifecycleKitUI`](../../Shared/LifecycleKitUI)'s container) gated in front of diff --git a/Where/WhereUI/SnapshotTests/AdaptiveHistoryLabViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/AdaptiveHistoryLabViewSnapshotTests.swift new file mode 100644 index 000000000..41199d6cf --- /dev/null +++ b/Where/WhereUI/SnapshotTests/AdaptiveHistoryLabViewSnapshotTests.swift @@ -0,0 +1,10 @@ +import SnapshotKitTesting +import Testing +@testable import WhereUI + +@MainActor +struct AdaptiveHistoryLabViewSnapshotTests { + @Test func adaptiveHistoryLab() async { + await assertSnapshots(of: AdaptiveHistoryLabView.self) + } +} diff --git a/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift index 16bd0120e..a647d697d 100644 --- a/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift @@ -1,10 +1,28 @@ import SnapshotKitTesting +import SwiftUI import Testing -@testable import WhereUI +@_spi(Testing) @testable import WhereUI @MainActor struct LocationsViewSnapshotTests { @Test func locations() async { await assertSnapshots(of: LocationsView.self) } + + @Test func adaptiveLoadStates() async throws { + for state in AdaptiveHistoryLoadFixture.State.allCases { + for pose in [AdaptiveHistoryPose.innerPortrait, .tabletop] { + let fixture = try await AdaptiveHistoryLoadFixture(state: state) + await assertSnapshots( + of: LocationsView(report: fixture.report) + .adaptiveHistoryReservedRegions(pose.regions) + .whereBroadwayRoot(), + named: "Adaptive-" + state.rawValue + "-" + pose.rawValue, + configurations: [SnapshotConfiguration(device: pose.frame)], + settle: .settledAtLeast(minDuration: 1), + ) + await fixture.finish() + } + } + } } diff --git a/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift index 0bb3d6d33..da105ae5a 100644 --- a/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift @@ -1,10 +1,28 @@ import SnapshotKitTesting +import SwiftUI import Testing -@testable import WhereUI +@_spi(Testing) @testable import WhereUI @MainActor struct YearViewSnapshotTests { @Test func year() async { await assertSnapshots(of: YearView.self) } + + @Test func adaptiveLoadStates() async throws { + for state in AdaptiveHistoryLoadFixture.State.allCases { + for pose in [AdaptiveHistoryPose.innerPortrait, .tabletop] { + let fixture = try await AdaptiveHistoryLoadFixture(state: state) + await assertSnapshots( + of: YearView(report: fixture.report) + .adaptiveHistoryReservedRegions(pose.regions) + .whereBroadwayRoot(), + named: "Adaptive-" + state.rawValue + "-" + pose.rawValue, + configurations: [SnapshotConfiguration(device: pose.frame)], + settle: .settledAtLeast(minDuration: 1), + ) + await fixture.finish() + } + } + } } diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png new file mode 100644 index 000000000..aa2a6b467 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c53b88bed3f6a401e13162c1a36c2c57238a39b0054e55186915425eb5cc9cf6 +size 976512 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png new file mode 100644 index 000000000..62871938c --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed752548de92a34849f5799e89a67c9e515c832770d4e26f225ccb143d04973e +size 2186307 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.HistoryColumn_historyColumn.png b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.HistoryColumn_historyColumn.png new file mode 100644 index 000000000..06580a6cb --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.HistoryColumn_historyColumn.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:039ab62cd46ee03977c31f57073646ecbcaf398b68f2544ec7603d664084ce57 +size 1797558 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png new file mode 100644 index 000000000..a5b09f928 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db3db1190f4920032b163fdc9dc2abafd7f4a64da561103e9f91a733ab7c6f9f +size 604511 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png new file mode 100644 index 000000000..91864358b --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2935de9dfba620bfc0c3cb7622ff2dabc7511bf0402a7eede6f9e86256ceeae2 +size 511264 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png new file mode 100644 index 000000000..b2cc0838f --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f59c4e1a48c974527e4cf787b69a2765f7900758ef0ead0012097224642058df +size 409110 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png new file mode 100644 index 000000000..119514268 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98245c8a9b51013c4afb0ad8fa380cb547643c791547f6e67ac117d6e87dff91 +size 447197 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png new file mode 100644 index 000000000..cb01f46d2 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78003267cd09c76eb61d1b86976fc1812f3132f99f12622b7ad4238a8902962c +size 675474 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png new file mode 100644 index 000000000..7abac70c8 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d64fe4a98304df550302df4fe6d291eb28115d30aed9f752f030d6a91a9f36f4 +size 731860 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.BrowseColumn_browseColumn.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.BrowseColumn_browseColumn.png new file mode 100644 index 000000000..10b2cba05 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.BrowseColumn_browseColumn.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61d783606c74c3039bdd25467ffd58584cb6048cd2c349cab0a8247ad30bddd6 +size 1484538 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png new file mode 100644 index 000000000..00340c259 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a98b418b4765c8d619a50b254885eabd6d41228591e172831da88cc0742ad817 +size 2769656 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png new file mode 100644 index 000000000..6d5df0f2c --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a30c09526929a833d63a1b1cd2f1ae069dc86f03f81230309ae7ecdcffb6e08 +size 2777708 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png new file mode 100644 index 000000000..6d5df0f2c --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a30c09526929a833d63a1b1cd2f1ae069dc86f03f81230309ae7ecdcffb6e08 +size 2777708 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png new file mode 100644 index 000000000..9a9adc287 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43f96bcdb2de942df6ecdb1117561f757928c4c27505f44c508909f67bf0f495 +size 2705770 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png new file mode 100644 index 000000000..e30dcdfed --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1ada5f99d1b8cb8ca0038f202cd765e9b02756ec88a00a579780489ad7be9fa +size 3114412 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_accessibility.png new file mode 100644 index 000000000..b5f6270c8 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6aeaa2e827174d03fbd7b24d6662b591c61fcd1ea142c7757b7d10f632d847d +size 3399725 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png new file mode 100644 index 000000000..73d920d13 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56665e9f5030ecf5b6533809018622e4ffba4a4f30b5d84580ec0f21351e028c +size 4264228 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png new file mode 100644 index 000000000..a0a232839 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a5f0c67ef577ca5f16acfcd6307a6ade4f21cc19f5a836d52b50439e32f8ff8 +size 3107305 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png new file mode 100644 index 000000000..8e069fee6 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f4cd5b8dc045ca65846c0dc8ce6b9dae9ea6ec8bb4dc347e65034aa6876ee8e +size 2291789 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png new file mode 100644 index 000000000..d841f9be6 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f0b7783770bc16582982976dc8033e2e7a4fd29501d5bd5c72d2715d1e5e34e +size 3108137 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-narrow_narrowWindow.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-narrow_narrowWindow.png new file mode 100644 index 000000000..75d2641dd --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-narrow_narrowWindow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92c14ee2a8fc6a1b406d4d9acd53b511d72c3775326320d3624bc1e42675b76b +size 1777621 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-outerLandscape_duoOuterLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-outerLandscape_duoOuterLandscape.png new file mode 100644 index 000000000..8aceeaba2 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-outerLandscape_duoOuterLandscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:360c781ec71559a20796b0fddeda6cee2cb293a9e29d0320ea859d947616a42e +size 2551980 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-outerPortrait_duoOuterPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-outerPortrait_duoOuterPortrait.png new file mode 100644 index 000000000..3cc1681aa --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-outerPortrait_duoOuterPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d0bda8f79942374a6f28b197f3a354bea1dfa1b164e2e4e1c8d6a464ed31621 +size 2684889 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png new file mode 100644 index 000000000..38536f1ff --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee8a80e3947a9a165d41859d49777f886fee835437da732258765de6918ff71b +size 1799650 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop_duoInnerPortrait.png new file mode 100644 index 000000000..0430f6cd1 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a239ae3a58fc4b79bedf3b3a7255b41817ccea5fa2df548bda626ebd730dafa2 +size 1813966 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-book_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-book_duoInnerLandscape.png new file mode 100644 index 000000000..47ce184ee --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-book_duoInnerLandscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37a536bb79570bc042b430741941d018e0e3a56691564b58a0edaf14d4339b6e +size 1094872 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerLandscape_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerLandscape_duoInnerLandscape.png new file mode 100644 index 000000000..47ce184ee --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerLandscape_duoInnerLandscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37a536bb79570bc042b430741941d018e0e3a56691564b58a0edaf14d4339b6e +size 1094872 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait-Selected_duoInnerPortrait.png new file mode 100644 index 000000000..09d488c2e --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait-Selected_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c542f155243936bb6c3e556eb71bf1973db2a10c2d6c0fe9c1ebc0a4aa2941d9 +size 2410888 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait.png new file mode 100644 index 000000000..13c7ff0d8 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41480f7c8d43a1865fa9f1b5e87a34b4db8377affc6314fb8c520f1585bd609d +size 1437913 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_accessibility.png new file mode 100644 index 000000000..11df5a612 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:899a49354dcb16fc739756ded98095f40969ced163b01df7a17905e862c34ac4 +size 1647970 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png new file mode 100644 index 000000000..33ef723ea --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b6975622141c150cfac5ee3a9f6aad12ae94a5c5239749a76bd1cdba7028f83 +size 313563 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png new file mode 100644 index 000000000..3469ab77d --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c01437248dba8e0795874375379d322decdbab1a7ad4e2066c6f15ebc37835e +size 1439418 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_dark.png new file mode 100644 index 000000000..42df66b0e --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cb553eef4f4a1204b09ac5d30f8c93691cce65c654b491367640dde53657565 +size 949799 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png new file mode 100644 index 000000000..fafb26276 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc326805c08f2add9977467c021666ab07bcb2028ce27759b8c65285096c598b +size 1414387 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-narrow_narrowWindow.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-narrow_narrowWindow.png new file mode 100644 index 000000000..ff1057928 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-narrow_narrowWindow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:862686df2ac36b2414bb12c3f5a709aa905c1add38e04b0041101fdf083dd848 +size 160662 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-outerLandscape_duoOuterLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-outerLandscape_duoOuterLandscape.png new file mode 100644 index 000000000..0bb1e59ab --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-outerLandscape_duoOuterLandscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de453ab504dea99e6a5de9308b760b3e37db7094065cc3672d55654bae4e1662 +size 156762 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-outerPortrait_duoOuterPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-outerPortrait_duoOuterPortrait.png new file mode 100644 index 000000000..8ca88b4d6 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-outerPortrait_duoOuterPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c7bc376fc86a0cfd9e04a48ae05088a6201d6ef4b43076cb6e611270b7730b0 +size 190202 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-tabletop_duoInnerPortrait.png new file mode 100644 index 000000000..13c7ff0d8 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-tabletop_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41480f7c8d43a1865fa9f1b5e87a34b4db8377affc6314fb8c520f1585bd609d +size 1437913 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png new file mode 100644 index 000000000..5f9a31574 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72ce7962efe0adcbe5eaadc8da0794cff6ecda629adc0992464d3bea9a1eaf95 +size 1692834 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png new file mode 100644 index 000000000..59489b05b --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5ebeb37eedbdefc3ef2129eb1a894ae0a7ad7bc79e5ad2f6a0cc54138e3a763 +size 2553279 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png new file mode 100644 index 000000000..59489b05b --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5ebeb37eedbdefc3ef2129eb1a894ae0a7ad7bc79e5ad2f6a0cc54138e3a763 +size 2553279 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png new file mode 100644 index 000000000..fa91c8633 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cb1843ee96ace4362668782f06a153f72ea073b8425162c2fb22e27b27559e2 +size 2157066 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png new file mode 100644 index 000000000..d14061e92 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19970a43c73b0512fac628d00dfed850062e1a3957dab8710e2da8826980bcd5 +size 3334286 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png new file mode 100644 index 000000000..4f7f0b7d9 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34b912d250b61eebc3f8d5bfa0d2b4435ddd7d96394c1e04f118f70dcf37522a +size 3536699 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png new file mode 100644 index 000000000..f5bbadf03 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e5e8f6ba98b55b56d460bdd2fc3e72bfb719e0792b6d8b9c96c0b6535f3df31 +size 1186574 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png new file mode 100644 index 000000000..dcef53106 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efbdca2fab2ac4fa090c7d78dfdd79bd5e4609649ec5ff82f5a59dc22e108ced +size 3343086 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png new file mode 100644 index 000000000..47a5e0526 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a2a035970f409c5f6062a0b5c23fe6b710c0a5c77e9f59aacaace1899e5026b +size 2104359 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png new file mode 100644 index 000000000..15fb6684c --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ed1b025bb6e46b9a838dc67d39f99673d2cc931eb074418ad78c35721d5dba6 +size 3035516 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-narrow_narrowWindow.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-narrow_narrowWindow.png new file mode 100644 index 000000000..b5fb99217 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-narrow_narrowWindow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7ab8ebe2b55ab14a8e68ec136920c6fbf3e3e6b1240d293ff23e79054a11166 +size 637706 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png new file mode 100644 index 000000000..8956893bb --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e37cb5bc7e86e0549f602c8d241ce7229e0884c63787eebf1998a59df22a5ce9 +size 297141 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerPortrait_duoOuterPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerPortrait_duoOuterPortrait.png new file mode 100644 index 000000000..fb2f2180a --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerPortrait_duoOuterPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f553698be31cb9980d9376f1b32782dc98a8d746f569651bf3fa9709859322e +size 851280 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png new file mode 100644 index 000000000..52042651b --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b36d1ebb49dceed7c5303104da2a3382d67e3afe0068bd517cfa8dd8f86f64c9 +size 723826 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png new file mode 100644 index 000000000..e7f6cd045 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6149d03f45bf3f9d1e55e66aa6a03e8d1f7c0700fbd4889a4191c642bf2dcb2 +size 462966 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png new file mode 100644 index 000000000..cdf5a00a6 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7428ac6d498ea4daac726ef3845b44a406a9af7ac5425754e621ffcdfc343ad3 +size 584230 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.BrowseColumn_browseColumn.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.BrowseColumn_browseColumn.png new file mode 100644 index 000000000..cbc3f7324 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.BrowseColumn_browseColumn.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d3d67dfb58120ade321f60414851ff40f7e114afc1f14a5f23af366f6bf3f52 +size 236358 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.PlannedStay_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.PlannedStay_iPhone_accessibility.png index 2f6c8b0c3..0010f4161 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.PlannedStay_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.PlannedStay_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb858be37ed69bb205e0134bbf409858d60c5006934cb44fa164154e45af65f2 -size 1768882 +oid sha256:b2a6c1fd9873d2300bb8ad3c7a8a3c2b400d1790816616293186daeda1e52de0 +size 1771581 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone_accessibility.png index 6d492b00f..9b5d4a267 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:513d58bb5bf968a62ccba15c55d99a3ad8f30aeaa33d091486e3fca8ee79d6cb -size 1806776 +oid sha256:c640130fe683dc621dd2df25d23ad6b823aab31ee99d9c020f9ca49230eb9a0a +size 1816444 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.BrowseColumn_browseColumn.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.BrowseColumn_browseColumn.png new file mode 100644 index 000000000..33fac6ad8 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.BrowseColumn_browseColumn.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:559497878d67e4a1dd8da32a8293dc5ef93ffeace04be5d9266491230c90536e +size 401212 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png new file mode 100644 index 000000000..3532566c8 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af520b49a3e327c99c991388f223b15e3f977bdfd100f9dfd0e2f39d0ae93110 +size 1453009 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png new file mode 100644 index 000000000..e07b7c8bd --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2685660d64f97858ce15e3f0db7dc0b47de9131daadf60fc5334461ae99483dc +size 344498 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png new file mode 100644 index 000000000..1af55b092 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e3fc62601804f0311e8b7d1fd8839b889b1044752bb56ecaba160ebb9d57b5f +size 1268756 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png new file mode 100644 index 000000000..6607a0bca --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99bd289479460605d15ae281fbca9dbd2920dabd40e7f06318f4e6bb57854075 +size 290708 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png new file mode 100644 index 000000000..5838866e8 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ca46cf79b975262e048c1d02bee2e4ae3487479ed9e21ea46dc1251833aff30 +size 1551314 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png new file mode 100644 index 000000000..4d4fa5be8 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f2a58fd04e81b9c440edb7fe169733c52dfb52cd66c49ec025056c08eb3d82c +size 569812 diff --git a/Where/WhereUI/Sources/Developer/DeveloperDestination.swift b/Where/WhereUI/Sources/Developer/DeveloperDestination.swift index 5fac751d6..fef508381 100644 --- a/Where/WhereUI/Sources/Developer/DeveloperDestination.swift +++ b/Where/WhereUI/Sources/Developer/DeveloperDestination.swift @@ -37,6 +37,7 @@ .tool(.logs), .tool(.openSpans), .flyover, + .tool(.adaptiveHistory), .tool(.regionMap), .tool(.crashTesting), ] diff --git a/Where/WhereUI/Sources/Developer/DeveloperOverlay.swift b/Where/WhereUI/Sources/Developer/DeveloperOverlay.swift index cc7abc891..f75b7888b 100644 --- a/Where/WhereUI/Sources/Developer/DeveloperOverlay.swift +++ b/Where/WhereUI/Sources/Developer/DeveloperOverlay.swift @@ -22,10 +22,9 @@ /// /// Attached once at ``RootView`` and compiled out of release (`#if DEBUG`). struct DeveloperOverlay: View { - /// The logged-in tab bar's height, measured by `MainTabs` and threaded in - /// via `RootView`, so the resting button clears the floating tab bar - /// without hardcoding its height. Zero when logged out. - private let tabBarInset: CGFloat + /// Native bar clearance measured on each edge by the active tab. + /// RootView passes it to this sibling overlay. Zero when logged out. + private let tabBarInset: EdgeInsets @State private var model: DeveloperOverlayModel @State private var dragOffset: CGSize = .zero @@ -43,24 +42,35 @@ @State private var windowResize: CGSize = .zero @Environment(\.stylesheet) private var stylesheet + // Gesture points and reserved-region frames use physical container coordinates. + @Environment(\.layoutDirection) private var layoutDirection @Environment(WhereDeveloperLaunchController.self) private var launchController: WhereDeveloperLaunchController? - init(tabBarInset: CGFloat = 0) { + init(tabBarInset: EdgeInsets = EdgeInsets()) { self.tabBarInset = tabBarInset _model = State(initialValue: DeveloperOverlayModel()) } - init(tabBarInset: CGFloat = 0, model: DeveloperOverlayModel) { + init(tabBarInset: EdgeInsets = EdgeInsets(), model: DeveloperOverlayModel) { self.tabBarInset = tabBarInset _model = State(initialValue: model) } var body: some View { - GeometryReader { proxy in + AdaptiveHistoryGeometryReader { geometry in + let leadingIsLeft = layoutDirection == .leftToRight + let preferred = CGPoint( + x: model.corner.isLeading == leadingIsLeft ? geometry.contentBounds + .minX : geometry.contentBounds.maxX, + y: model.corner.isTop ? geometry.contentBounds.minY : geometry.contentBounds + .maxY, + ) + let area = geometry.unobstructedBounds(near: preferred) + let container = area.size let style = stylesheet.developerOverlay - let anchor = anchorPoint(for: model.corner, in: proxy.size) - let menuFrame = menuFrame(anchor: anchor, in: proxy.size) + let anchor = anchorPoint(for: model.corner, in: container) + let menuFrame = menuFrame(anchor: anchor, in: container) ZStack { if model.presentation.isFullScreen { @@ -94,7 +104,7 @@ panel( tool: tool, isFullScreen: model.presentation.isFullScreen, - in: proxy, + in: container, ) } @@ -109,12 +119,14 @@ y: anchor.y + dragOffset.height, ) .simultaneousGesture( - dragGesture(in: proxy), + dragGesture(in: container), isEnabled: model.presentation == .collapsed, ) .transition(.scale.combined(with: .opacity)) } } + .frame(width: area.width, height: area.height) + .position(x: area.midX, y: area.midY) .animation(style.presentationAnimation, value: model.presentation) .accessibilityElement(children: .contain) .accessibilityAddTraits( @@ -124,9 +136,10 @@ // can inset the app content behind it. .preference( key: DeveloperOverlayInsetKey.self, - value: appContentInsets(in: proxy.size), + value: appContentInsets(in: geometry.contentBounds.size, area: area), ) } + .padding(tabBarInset) .fullScreenCover(isPresented: $isPresentingFlyover) { WhereFlyoverPresentationView() } @@ -185,21 +198,25 @@ } } - private func dragGesture(in proxy: GeometryProxy) -> some Gesture { + private func dragGesture(in container: CGSize) -> some Gesture { DragGesture(minimumDistance: 8) .onChanged { value in isDraggingButton = true dragOffset = value.translation } .onEnded { value in - let anchor = anchorPoint(for: model.corner, in: proxy.size) + let anchor = anchorPoint(for: model.corner, in: container) let dropPoint = CGPoint( x: anchor.x + value.translation.width, y: anchor.y + value.translation.height, ) let newCorner = DeveloperOverlayModel.nearestCorner( - to: dropPoint, - in: proxy.size, + to: CGPoint( + x: layoutDirection == .leftToRight ? dropPoint.x : container + .width - dropPoint.x, + y: dropPoint.y, + ), + in: container, ) withAnimation(stylesheet.developerOverlay.presentationAnimation) { model.setCorner(newCorner) @@ -214,10 +231,7 @@ } } - /// Resting center for the button in a given corner. `size` is already the - /// safe-area region (the `GeometryReader` respects the safe area), so the - /// only extra offset is the measured tab-bar height on the bottom corners - /// so the button clears the floating tab bar when logged in. + /// The container already excludes the window and tab bar's independent edges. private func anchorPoint( for corner: DeveloperOverlayModel.Corner, in size: CGSize, @@ -225,10 +239,12 @@ let halfWidth = buttonSize.width / 2 let halfHeight = buttonSize.height / 2 let edgeInset = stylesheet.developerOverlay.edgeInset - let leadingX = edgeInset + halfWidth - let trailingX = size.width - edgeInset - halfWidth + let leftX = edgeInset + halfWidth + let rightX = size.width - edgeInset - halfWidth + let leadingX = layoutDirection == .leftToRight ? leftX : rightX + let trailingX = layoutDirection == .leftToRight ? rightX : leftX let topY = edgeInset + halfHeight - let bottomY = size.height - tabBarInset - edgeInset - halfHeight + let bottomY = size.height - edgeInset - halfHeight switch corner { case .topLeading: return CGPoint(x: leadingX, y: topY) case .topTrailing: return CGPoint(x: trailingX, y: topY) @@ -254,7 +270,7 @@ endY = anchor.y - buttonHalfHeight - style.menu.launcherSpacing } let height = max(endY - startY, 0) - let minX = model.corner.isLeading + let minX = model.corner.isLeading == (layoutDirection == .leftToRight) ? style.edgeInset : container.width - style.edgeInset - menuWidth return CGRect(x: minX, y: startY, width: menuWidth, height: height) @@ -263,13 +279,13 @@ private func panel( tool: DeveloperTool, isFullScreen: Bool, - in proxy: GeometryProxy, + in container: CGSize, ) -> some View { let style = stylesheet.developerOverlay - let layout = displayedLayout(in: proxy.size) - let size = isFullScreen ? fullScreenSize(in: proxy.size) : layout.size + let layout = displayedLayout(in: container) + let size = isFullScreen ? fullScreenSize(in: container) : layout.size let center = isFullScreen - ? CGPoint(x: proxy.size.width / 2, y: proxy.size.height / 2) + ? CGPoint(x: container.width / 2, y: container.height / 2) : layout.center let shape = RoundedRectangle(cornerRadius: style.panel.cornerRadius) return DeveloperSurface( @@ -282,10 +298,10 @@ withAnimation(style.presentationAnimation) { model.closeTool() } }, onMove: { translation, ended in - moveWindow(by: translation, ended: ended, in: proxy.size) + moveWindow(by: translation, ended: ended, in: container) }, onResize: { translation, ended in - resizeWindow(by: translation, ended: ended, in: proxy.size) + resizeWindow(by: translation, ended: ended, in: container) }, ) .frame(width: size.width, height: size.height) @@ -387,11 +403,14 @@ /// app content clear of it. Zero unless floating; the docking math + cap /// live in ``DeveloperOverlayModel/contentInsets(for:in:edgeTolerance:)``, /// fed the on-screen (clamped) `displayedLayout`. - private func appContentInsets(in container: CGSize) -> EdgeInsets { + private func appContentInsets(in container: CGSize, area: CGRect) -> EdgeInsets { guard case .floating = model.presentation else { return EdgeInsets() } let style = stylesheet.developerOverlay + var layout = displayedLayout(in: area.size) + layout.center.x += area.minX + layout.center.y += area.minY return DeveloperOverlayModel.contentInsets( - for: displayedLayout(in: container), + for: layout, in: container, edgeTolerance: style.edgeInset, style: style.floatingWindow, diff --git a/Where/WhereUI/Sources/Developer/DeveloperTabBarInset.swift b/Where/WhereUI/Sources/Developer/DeveloperTabBarInset.swift index 8e04ab950..4e6c8f42a 100644 --- a/Where/WhereUI/Sources/Developer/DeveloperTabBarInset.swift +++ b/Where/WhereUI/Sources/Developer/DeveloperTabBarInset.swift @@ -1,7 +1,7 @@ import SwiftUI extension View { - /// Report the logged-in tab bar's height up the tree so the DEBUG developer + /// Report the logged-in tab bar's edge insets up the tree so the DEBUG developer /// overlay can rest its collapsed button clear of the floating tab bar, /// measured from the live UI instead of a hardcoded guess. Applied to each /// tab's content in `MainTabs`. A no-op in release, where the overlay (and the @@ -18,19 +18,20 @@ extension View { #if DEBUG import UIKit - /// Carries the floating tab bar's height from `MainTabs` up to ``RootView``, + /// Carries the floating tab bar's edge insets from `MainTabs` up to ``RootView``, /// which hands it to the sibling ``DeveloperOverlay``. /// - /// A tab's content receives a bottom safe-area inset that spans the home - /// indicator *and* the floating tab bar; subtracting the window's own bottom - /// inset (the home indicator alone) leaves just the bar's height. Reduced with - /// `max` so whichever tab is on screen wins. When logged out there's no tab - /// bar in the tree, so the value stays at its `0` default. + /// Subtract the attached window's system insets independently on every edge. + /// Reduce each edge with `max` so the visible tab wins. Logged-out scenes report zero. struct DeveloperTabBarInsetKey: PreferenceKey { - static let defaultValue: CGFloat = 0 + static let defaultValue = EdgeInsets() - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value = max(value, nextValue()) + static func reduce(value: inout EdgeInsets, nextValue: () -> EdgeInsets) { + let next = nextValue() + value.top = max(value.top, next.top) + value.leading = max(value.leading, next.leading) + value.bottom = max(value.bottom, next.bottom) + value.trailing = max(value.trailing, next.trailing) } } @@ -53,27 +54,99 @@ extension View { } private struct DeveloperTabBarInsetReporter: ViewModifier { + @State private var windowInsets = UIEdgeInsets.zero + /// The attached window supplies physical insets; convert them using the live direction. + @Environment(\.layoutDirection) private var layoutDirection + func body(content: Content) -> some View { content.background { GeometryReader { proxy in - Color.clear.preference( - key: DeveloperTabBarInsetKey.self, - value: max(0, proxy.safeAreaInsets.bottom - Self.windowBottomInset), - ) + Color.clear + .background { + DeveloperWindowInsetsReader { windowInsets = $0 } + } + .preference( + key: DeveloperTabBarInsetKey.self, + value: DeveloperBarInsets.additional( + content: proxy.safeAreaInsets, + window: windowInsets, + direction: layoutDirection, + ), + ) } } } + } + + /// Subtract only this host window's system edges, including a side-mounted status bar. + enum DeveloperBarInsets { + static func additional( + content: EdgeInsets, + window: UIEdgeInsets, + direction: LayoutDirection, + ) -> EdgeInsets { + EdgeInsets( + top: max(0, content.top - window.top), + leading: max( + 0, + content.leading - (direction == .leftToRight ? window.left : window.right), + ), + bottom: max(0, content.bottom - window.bottom), + trailing: max( + 0, + content.trailing - (direction == .leftToRight ? window.right : window.left), + ), + ) + } + } + + /// Reads the window containing this reporter. It never searches other scenes or screens. + private struct DeveloperWindowInsetsReader: UIViewRepresentable { + let changed: (UIEdgeInsets) -> Void + + func makeUIView(context _: Context) -> InsetView { + InsetView() + } + + func updateUIView(_ view: InsetView, context _: Context) { + view.changed = changed + view.scheduleUpdate() + } + + static func dismantleUIView(_ view: InsetView, coordinator _: Void) { + view.pending?.cancel() + view.changed = nil + } + + final class InsetView: UIView { + var changed: ((UIEdgeInsets) -> Void)? + var pending: Task? + private var reported: UIEdgeInsets? + + override func didMoveToWindow() { + super.didMoveToWindow() + scheduleUpdate() + } + + override func safeAreaInsetsDidChange() { + super.safeAreaInsetsDidChange() + scheduleUpdate() + } - /// The window's own bottom safe-area inset (the home indicator), which the - /// tab content's inset sits on top of. Read from the key window because the - /// reporter lives inside the tab bar's inset and can't see the bare window - /// inset itself. - @MainActor private static var windowBottomInset: CGFloat { - UIApplication.shared.connectedScenes - .compactMap { $0 as? UIWindowScene } - .flatMap(\.windows) - .first { $0.isKeyWindow }? - .safeAreaInsets.bottom ?? 0 + override func layoutSubviews() { + super.layoutSubviews() + scheduleUpdate() + } + + func scheduleUpdate() { + guard let insets = window?.safeAreaInsets, reported != insets else { return } + pending?.cancel() + pending = Task { @MainActor [weak self] in + guard !Task.isCancelled, let self, window != nil else { return } + reported = insets + changed?(insets) + } + } } } #endif diff --git a/Where/WhereUI/Sources/Developer/DeveloperTool.swift b/Where/WhereUI/Sources/Developer/DeveloperTool.swift index 12c613a50..94be065fa 100644 --- a/Where/WhereUI/Sources/Developer/DeveloperTool.swift +++ b/Where/WhereUI/Sources/Developer/DeveloperTool.swift @@ -12,6 +12,7 @@ case logs case openSpans case regionMap + case adaptiveHistory var id: Self { self @@ -27,6 +28,8 @@ String(localized: .developerOpenSpansLink) case .regionMap: String(localized: .developerRegionMapLink) + case .adaptiveHistory: + String(localized: .developerDuoTitle) } } @@ -36,6 +39,7 @@ case .logs: .ladybug case .openSpans: .timer case .regionMap: .map + case .adaptiveHistory: .rectangleSplit2x1 } } } diff --git a/Where/WhereUI/Sources/Developer/DeveloperToolView.swift b/Where/WhereUI/Sources/Developer/DeveloperToolView.swift index 63c5f2495..c24bcbd65 100644 --- a/Where/WhereUI/Sources/Developer/DeveloperToolView.swift +++ b/Where/WhereUI/Sources/Developer/DeveloperToolView.swift @@ -20,49 +20,59 @@ import SFSafeSymbols @Environment(WhereModel.self) private var model: WhereModel? var body: some View { - NavigationStack { - switch tool { - case .crashTesting: - DeveloperCrashTestingView() + if tool == .adaptiveHistory { + toolContent + } else { + NavigationStack { toolContent } + } + } + + @ViewBuilder + private var toolContent: some View { + switch tool { + case .crashTesting: + DeveloperCrashTestingView() + + case .logs: + switch model?.logStoreState { + case let .ready(store): + PeriscopeViewer( + store: store, + title: String(localized: .developerLogsTitle), + ) - case .logs: - switch model?.logStoreState { - case let .ready(store): - PeriscopeViewer( - store: store, - title: String(localized: .developerLogsTitle), - ) + case .opening: + ContentUnavailableView( + String(localized: .developerLoggingOpeningTitle), + systemSymbol: tool.systemSymbol, + description: Text(String( + localized: .developerLoggingOpeningDescription, + )), + ) + .navigationTitle(tool.title) + .navigationBarTitleDisplayMode(.inline) - case .opening: - ContentUnavailableView( - String(localized: .developerLoggingOpeningTitle), - systemSymbol: tool.systemSymbol, - description: Text(String( - localized: .developerLoggingOpeningDescription, - )), - ) - .navigationTitle(tool.title) - .navigationBarTitleDisplayMode(.inline) + case let .failed(description): + ContentUnavailableView( + String(localized: .developerLoggingFailedTitle), + systemSymbol: .exclamationmarkTriangle, + description: Text(description), + ) + .navigationTitle(tool.title) + .navigationBarTitleDisplayMode(.inline) - case let .failed(description): - ContentUnavailableView( - String(localized: .developerLoggingFailedTitle), - systemSymbol: .exclamationmarkTriangle, - description: Text(description), - ) - .navigationTitle(tool.title) - .navigationBarTitleDisplayMode(.inline) + case .unavailable, nil: + DeveloperToolUnavailableView(tool: tool) + } - case .unavailable, nil: - DeveloperToolUnavailableView(tool: tool) - } + case .openSpans: + OpenSpansView(system: .shared) - case .openSpans: - OpenSpansView(system: .shared) + case .regionMap: + RegionMapView() - case .regionMap: - RegionMapView() - } + case .adaptiveHistory: + AdaptiveHistoryLabView() } } } diff --git a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabModel.swift b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabModel.swift new file mode 100644 index 000000000..d40c89382 --- /dev/null +++ b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabModel.swift @@ -0,0 +1,39 @@ +#if DEBUG + import Observation + + /// Builds one separate demo world and retains its scene through every synthetic pose. + @MainActor @Observable + final class AdaptiveHistoryLabModel { + enum State { + case idle + case loading + case loaded(WhereFlyoverWorld) + case failed(Error) + } + + var pose = AdaptiveHistoryPose.outerPortrait + let navigation = SceneNavigationModel() + private(set) var state = State.idle + private(set) var request = 0 + + func load() async { + guard case .idle = state else { return } + state = .loading + do { + let world = try await WhereFlyoverWorld.build() + try Task.checkCancellation() + state = .loaded(world) + } catch is CancellationError { + state = .idle + } catch { + state = .failed(error) + } + } + + func retry() { + guard case .failed = state else { return } + state = .idle + request += 1 + } + } +#endif diff --git a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift new file mode 100644 index 000000000..cd77c3e53 --- /dev/null +++ b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift @@ -0,0 +1,124 @@ +#if DEBUG + import SnapshotKit + import SwiftUI + + /// A DEBUG-only mounted production scene. Pose controls change geometry, never its models. + struct AdaptiveHistoryLabView: View { + @State private var model: AdaptiveHistoryLabModel + + init(model: AdaptiveHistoryLabModel = AdaptiveHistoryLabModel()) { + _model = State(initialValue: model) + } + + @Environment(\.stylesheet) private var stylesheet + + var body: some View { + @Bindable var model = model + VStack(spacing: stylesheet.spacing.medium) { + Text(String(localized: .developerDuoTitle)) + .font(.headline) + Picker(String(localized: .developerDuoPose), selection: $model.pose) { + ForEach(AdaptiveHistoryPose.allCases) { pose in + Text(pose.title).tag(pose) + } + } + .pickerStyle(.menu) + Group { + switch model.state { + case .idle, .loading: + ProgressView(String(localized: .primaryLoading)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + case let .loaded(world): + preview(world: world) + case let .failed(error): + ContentUnavailableView { + Label( + String(localized: .commonLoadErrorTitle), + systemSymbol: .exclamationmarkTriangle, + ) + } description: { + Text(error.localizedDescription) + } actions: { + Button(String(localized: .developerDuoRetry), action: model.retry) + } + } + } + Text(String(localized: .developerDuoNote)) + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + .padding(stylesheet.spacing.medium) + .background(.background) + .task(id: model.request) { await model.load() } + } + + private func preview(world: WhereFlyoverWorld) -> some View { + GeometryReader { proxy in + let pose = model.pose + let insets = pose.frame.safeAreaInsets + let scale = min( + 1, + min(proxy.size.width / pose.size.width, proxy.size.height / pose.size.height), + ) + MainTabs(world: world, navigation: model.navigation) + .environment(world.model) + .environment(world.session) + .environment(\.isInDemoMode, true) + .environment(\.demoExitPresentation, .embedded) + .padding(EdgeInsets( + top: insets.top, + leading: insets.leading, + bottom: insets.bottom, + trailing: insets.trailing, + )) + .frame(width: pose.size.width, height: pose.size.height) + .background(.background) + .adaptiveHistoryReservedRegions(pose.regions) + .whereBroadwayRoot() + .snapshotTraits(SnapshotConfiguration(device: pose.frame)) + .frame(width: pose.size.width, height: pose.size.height) + .scaleEffect(scale, anchor: .top) + .frame(width: proxy.size.width, height: proxy.size.height, alignment: .top) + .clipped() + } + } + } + + extension AdaptiveHistoryLabView: SnapshotProviding { + static var snapshots: [SnapshotCase] { + [AdaptiveHistoryPose.outerPortrait, .tabletop].map { pose in + let fixture = AdaptiveHistorySnapshotFixture(tab: .locations, selection: .region) + fixture.model.pose = pose + return whereSnapshot( + name: pose.rawValue, + configurations: [SnapshotConfiguration(device: .viewport( + name: "iPad", + size: CGSize(width: 834, height: 1194), + layoutTraits: .init( + horizontalSizeClass: .regular, + verticalSizeClass: .regular, + interfaceIdiom: .pad, + ), + ))], + onReadyToSnapshot: { await fixture.prepare() }, + ) { + AdaptiveHistoryLabView(model: fixture.model) + .environment(\.regionOutlinePathCache, fixture.artwork) + } + } + } + } + + #Preview { AdaptiveHistoryLabView.snapshotPreviews } + + extension AdaptiveHistoryLabView: WhereFlyoverProviding { + static let flyoverData = WhereFlyoverData.hosted( + AdaptiveHistoryLabView.self, + title: "Duo Layout Lab", + navigationContainer: .none, + ) { _ in + AdaptiveHistoryLabView() + } + } +#endif diff --git a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistorySnapshotFixture.swift b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistorySnapshotFixture.swift new file mode 100644 index 000000000..6a6e0bb47 --- /dev/null +++ b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistorySnapshotFixture.swift @@ -0,0 +1,127 @@ +#if DEBUG + import SnapshotKit + import SwiftUI + import WhereCore + + /// Each capture configuration gets its own demo world, navigation, and artwork readiness. + @MainActor + final class AdaptiveHistorySnapshotFixture { + static let browseColumn = SnapshotConfiguration.Frame.fullContent( + name: "browseColumn", + width: WhereStylesheet.default.adaptiveHistory.minimumControlsWidth, + minimumHeight: 669, + layoutTraits: .init( + horizontalSizeClass: .regular, + verticalSizeClass: .regular, + interfaceIdiom: .phone, + ), + ) + static let historyColumn = SnapshotConfiguration.Frame.fullContent( + name: "historyColumn", + width: WhereStylesheet.default.adaptiveHistory.minimumHistoryWidth, + minimumHeight: 669, + layoutTraits: .init( + horizontalSizeClass: .regular, + verticalSizeClass: .regular, + interfaceIdiom: .phone, + ), + ) + + enum Selection: Equatable { + case none + case region + case stay + case month(CalendarDay) + case settings + } + + let model = AdaptiveHistoryLabModel() + let artwork = RegionOutlinePathCache() + private let selection: Selection + private var didSelect = false + + init(tab: SceneNavigationModel.TabID, selection: Selection) { + model.navigation.selectedTab = tab + self.selection = selection + } + + func prepare() async { + await model.load() + while case .loading = model.state { + guard !Task.isCancelled else { return } + await Task.yield() + } + guard case let .loaded(world) = model.state else { return } + if !didSelect { + didSelect = true + switch selection { + case .none: break + case .region: + model.navigation.locations.select(region: .california) + case .stay: + if let report = world.report.report, + let stint = PresenceTimeline.stints( + from: report, + calendar: world.report.calendar, + ).first + { + model.navigation.year.history.select( + stint: stint, + dateCalendar: world.report.calendar, + ) + } + case let .month(month): + model.navigation.year.history.select( + month: month, + dateCalendar: world.report.calendar, + ) + case .settings: + model.navigation.settings.selectedRoute = SettingsRoute(.appearance) + } + } + for region in LocationsBackgroundArtwork.regions(in: world.report.ranking) { + _ = await artwork.path(for: region, resolution: .small) + _ = await artwork.path(for: region, resolution: .medium) + _ = await artwork.path(for: region, resolution: .micro) + } + } + } + + #Preview { + AdaptiveHistorySnapshotScene( + fixture: AdaptiveHistorySnapshotFixture(tab: .locations, selection: .region), + pose: .innerPortrait, + ) + .whereBroadwayRoot() + .snapshotTraits(SnapshotConfiguration(device: .duoInnerPortrait)) + } + + struct AdaptiveHistorySnapshotScene: View { + let fixture: AdaptiveHistorySnapshotFixture + let pose: AdaptiveHistoryPose + + var body: some View { + Group { + switch fixture.model.state { + case .idle, .loading: + ProgressView(String(localized: .primaryLoading)) + case let .loaded(world): + MainTabs(world: world, navigation: fixture.model.navigation) + .environment(world.model) + .environment(world.session) + .environment(\.isInDemoMode, true) + .environment(\.demoExitPresentation, .embedded) + .environment(\.regionOutlinePathCache, fixture.artwork) + case let .failed(error): + ContentUnavailableView( + String(localized: .commonLoadErrorTitle), + systemSymbol: .exclamationmarkTriangle, + description: Text(error.localizedDescription), + ) + } + } + .adaptiveHistoryReservedRegions(pose.regions) + .task { await fixture.prepare() } + } + } +#endif diff --git a/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverCatalog.swift b/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverCatalog.swift index cdb86dc58..f527d71a2 100644 --- a/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverCatalog.swift +++ b/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverCatalog.swift @@ -120,6 +120,7 @@ WhereFlyoverLogView.flyoverData, OpenSpansView.flyoverData, RegionMapView.flyoverData, + AdaptiveHistoryLabView.flyoverData, ] } diff --git a/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverHost.swift b/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverHost.swift index 322e77a77..054ced184 100644 --- a/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverHost.swift +++ b/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverHost.swift @@ -11,6 +11,7 @@ .environment(world.model) .environment(world.session) .environment(\.isInDemoMode, true) + .environment(\.demoExitPresentation, .embedded) .whereBroadwayRoot(regionStyles: world.session.regionStyles) } } diff --git a/Where/WhereUI/Sources/MainTabs.swift b/Where/WhereUI/Sources/MainTabs.swift index fde47cbfb..9d2392087 100644 --- a/Where/WhereUI/Sources/MainTabs.swift +++ b/Where/WhereUI/Sources/MainTabs.swift @@ -21,8 +21,7 @@ import SwiftUI /// checked wiring); the always-on `WhereSession` coordinator stays in the /// environment. struct MainTabs: View { - /// Identity for the tab-bar selection. - private typealias TabID = MainTabSelection.TabID + private typealias TabID = SceneNavigationModel.TabID private struct WelcomeTaskID: Hashable { let isActive: Bool @@ -33,7 +32,7 @@ struct MainTabs: View { @State private var recordingWarning: RecordingConfigurationWarningModel @State private var welcome: LocationWelcomeModel @State private var planningDestination: PlannedStaysDestination? - @State private var selection = MainTabSelection() + @State private var navigation = SceneNavigationModel() @Environment(UpdateModel.self) private var updates: UpdateModel? @Environment(\.isInDemoMode) private var isInDemoMode @Environment(\.scenePhase) private var scenePhase @@ -66,14 +65,19 @@ struct MainTabs: View { } var body: some View { - TabView(selection: $selection.selected) { + @Bindable var navigation = navigation + TabView(selection: $navigation.selectedTab) { Tab( String(localized: .tabLocations), systemSymbol: .locationFill, value: TabID.locations, ) { - LocationsView(report: report, openElsewhere: { selection.selected = .elsewhere }) - .reportingDeveloperTabBarInset() + LocationsView( + report: report, + navigation: navigation.locations, + openElsewhere: { navigation.selectedTab = .elsewhere }, + ) + .reportingDeveloperTabBarInset() } if report.hasElsewhere { @@ -82,19 +86,23 @@ struct MainTabs: View { systemSymbol: .globeAmericas, value: TabID.elsewhere, ) { - ElsewhereView(report: report, isSelected: selection.selected == .elsewhere) + ElsewhereView(report: report, isSelected: navigation.selectedTab == .elsewhere) .reportingDeveloperTabBarInset() } } Tab(String(localized: .tabYear), systemSymbol: .calendar, value: TabID.year) { - YearView(report: report) + YearView(report: report, navigation: navigation.year) .reportingDeveloperTabBarInset() } Tab(value: TabID.settings) { - SettingsView(report: report, recordingWarning: recordingWarning) - .reportingDeveloperTabBarInset() + SettingsView( + report: report, + recordingWarning: recordingWarning, + navigation: navigation.settings, + ) + .reportingDeveloperTabBarInset() } label: { RecordingConfigurationWarningTabLabel( model: recordingWarning, @@ -107,7 +115,7 @@ struct MainTabs: View { } .accessibilityHidden(welcomePresentation != nil) .onChange(of: report.hasElsewhere, initial: true) { _, hasElsewhere in - selection.reconcile(hasElsewhere: hasElsewhere) + navigation.reconcile(hasElsewhere: hasElsewhere) } .tabViewBottomAccessory(isEnabled: welcomeAccessory != nil) { if let welcomeAccessory { @@ -206,7 +214,22 @@ struct MainTabs: View { preferences: recordingWarningSource.preferences, )) _welcome = State(initialValue: welcome) - _selection = State(initialValue: MainTabSelection(selected: selection)) + _navigation = State(initialValue: SceneNavigationModel(selectedTab: selection)) + } + + /// Retains the production shell around a separately built in-memory lab world. + init(world: WhereFlyoverWorld, navigation: SceneNavigationModel) { + self.init( + session: world.session, + report: world.report, + welcome: LocationWelcomeModel( + services: world.scope.services, + preferences: world.scope.preferences, + now: world.session.now, + ), + selection: navigation.selectedTab, + ) + _navigation = State(initialValue: navigation) } #endif } @@ -227,7 +250,7 @@ struct MainTabs: View { configurations: [configuration], settle: .settledAtLeast(minDuration: 1.0), ) - } + [elsewhereSnapshot(), elsewhereSnapshot(theme: .alternate)] + } + [elsewhereSnapshot(), elsewhereSnapshot(theme: .alternate)] + adaptiveSnapshots } private static func elsewhereSnapshot(theme: WhereTheme = .standard) -> SnapshotCase { @@ -311,6 +334,105 @@ struct MainTabs: View { } } + /// Viewport composition supplements the full-content Locations, calendar, timeline, + /// and Settings matrices. Synthetic regions exercise the production layout policy. + private static var adaptiveSnapshots: [SnapshotCase] { + let tabs: [SceneNavigationModel.TabID] = [.locations, .year, .settings] + var cases = tabs.flatMap { tab in + AdaptiveHistoryPose.allCases.map { pose in + adaptiveSnapshot( + tab: tab, + pose: pose, + configuration: SnapshotConfiguration(device: pose.frame), + selection: .none, + ) + } + } + for tab in tabs { + let pose = AdaptiveHistoryPose.innerPortrait + let configurations = [ + SnapshotConfiguration(colorScheme: .dark, device: pose.frame), + SnapshotConfiguration(dynamicType: .accessibility5, device: pose.frame), + SnapshotConfiguration(layoutDirection: .rightToLeft, device: pose.frame), + SnapshotConfiguration(contrast: .increased, device: pose.frame), + SnapshotConfiguration(device: pose.frame, snapshotType: .accessibility), + ] + cases += configurations.map { configuration in + adaptiveSnapshot( + tab: tab, + pose: pose, + configuration: configuration, + selection: .none, + ) + } + } + for pose in [AdaptiveHistoryPose.innerPortrait, .tabletop, .book] { + cases.append(adaptiveSnapshot( + tab: .locations, + pose: pose, + configuration: SnapshotConfiguration(device: pose.frame), + selection: .region, + )) + cases.append(adaptiveSnapshot( + tab: .year, + pose: pose, + configuration: SnapshotConfiguration(device: pose.frame), + selection: .stay, + )) + } + cases.append(adaptiveSnapshot( + tab: .settings, + pose: .innerPortrait, + configuration: SnapshotConfiguration(device: .duoInnerPortrait), + selection: .settings, + )) + // May 2026 occupies six rows in the demo's Sunday-first calendar. + cases.append(adaptiveSnapshot( + tab: .year, + pose: .tabletop, + configuration: SnapshotConfiguration(device: .duoInnerPortrait), + selection: .month(CalendarDay(year: PreviewSupport.year, month: 5, day: 1)), + )) + return cases + } + + private static func adaptiveSnapshot( + tab: SceneNavigationModel.TabID, + pose: AdaptiveHistoryPose, + configuration: SnapshotConfiguration, + selection: AdaptiveHistorySnapshotFixture.Selection, + ) -> SnapshotCase { + let tabName = switch tab { + case .locations: "Locations" + case .elsewhere: "Elsewhere" + case .year: "Year" + case .settings: "Settings" + } + let fixture = AdaptiveHistorySnapshotFixture(tab: tab, selection: selection) + let selectionName = switch selection { + case .none: "" + case .region, .stay, .settings: "-Selected" + case .month: "-Month" + } + return whereSnapshot( + name: "Adaptive-" + tabName + "-" + pose.rawValue + selectionName, + configurations: [configuration], + // Native glass can retain its light material after the async + // shell appears; match the measured Appearance/About floor. + settle: .settledAtLeast(minDuration: 1.5), + onReadyToSnapshot: { + await fixture.prepare() + // Request the fixture's starting position after demo preparation; + // native navigation insets can change while the shell appears. + if tab == .locations { + fixture.model.navigation.locations.scrollPosition.scrollTo(edge: .top) + } + }, + ) { + AdaptiveHistorySnapshotScene(fixture: fixture, pose: pose) + } + } + private static func snapshotCases( configurations: [SnapshotConfiguration], settle: SnapshotSettle, diff --git a/Where/WhereUI/Sources/Primary/CalendarContentView.swift b/Where/WhereUI/Sources/Primary/CalendarContentView.swift index bf0f8bb1a..78a30fa92 100644 --- a/Where/WhereUI/Sources/Primary/CalendarContentView.swift +++ b/Where/WhereUI/Sources/Primary/CalendarContentView.swift @@ -11,6 +11,12 @@ import SwiftUI /// navigation: the Your Year tab embeds it inline, and the Locations tab pushes /// it — region-focused, with a title — as the zoom destination of a tapped card. struct CalendarContentView: View { + enum Presentation { case year, month } + let history: HistoryNavigationModel? + let displayMode: Presentation + let ownsPlanningPresentation: Bool + @State private var navigation: CalendarNavigationModel + /// When set, the day grid only shows dots for this region (so it reads as /// "just the days I spent here"); the per-month footer still lists every /// region. `nil` shows every region's dots. @@ -20,7 +26,6 @@ struct CalendarContentView: View { @Environment(\.stylesheet) private var stylesheet @State private var monthsLoad: Result<[CalendarMonth], Error>? - @State private var planningDestination: PlannedStaysDestination? @State private var presentation: CalendarPresentationModel @AccessibilityFocusState private var focusedTarget: CalendarPresentationModel.Target? @State private var zoom = CalendarZoomTransitionModel() @@ -30,21 +35,26 @@ struct CalendarContentView: View { init( focusedRegion: Region? = nil, report: YearReportModel, + navigation: CalendarNavigationModel? = nil, + history: HistoryNavigationModel? = nil, + presentation: Presentation = .year, initialMode: CalendarPresentationModel.Mode = .detail, persistenceKey: CalendarZoomKey? = nil, ) { self.focusedRegion = focusedRegion self.report = report - _presentation = State(initialValue: CalendarPresentationModel( - mode: initialMode, - preferences: report.preferences, + self.history = history + displayMode = presentation + ownsPlanningPresentation = navigation == nil && history == nil + let owner = history?.calendar ?? navigation ?? CalendarNavigationModel() + _navigation = State(initialValue: owner) + _presentation = State(initialValue: owner.presentation( + report: report, + initialMode: initialMode, persistenceKey: persistenceKey, )) } - @State private var scrollPosition = ScrollPosition(idType: CalendarPresentationModel.Target - .self) - private static let logger = WhereLog.session(CalendarViewLog.self) /// Inputs that invalidate a cached month grid. @@ -109,23 +119,28 @@ struct CalendarContentView: View { // Log View Mode: reveal an inspect badge for this calendar's events. A // no-op in release. .debugLogInspectable(WhereLog.session(CalendarViewLog.self)) - .sheet(item: $planningDestination) { destination in - PlannedStaysDestinationView(destination: destination, report: report) - } + .modifier(PlanningPresentation( + report: report, + navigation: navigation.planning, + isEnabled: ownsPlanningPresentation, + )) + .toolbar(displayMode == .month ? .hidden : .automatic, for: .navigationBar) .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button { - changeZoom { presentation.toggle() } - } label: { - Label( - presentation - .mode == .detail ? String(localized: .calendarShowYear) : - String(localized: .calendarShowMonths), - systemSymbol: presentation - .mode == .detail ? .squareGrid3x3 : .rectangleGrid1x2, - ) + if displayMode == .year { + ToolbarItem(placement: .topBarTrailing) { + Button { + changeZoom { presentation.toggle() } + } label: { + Label( + presentation + .mode == .detail ? String(localized: .calendarShowYear) : + String(localized: .calendarShowMonths), + systemSymbol: presentation + .mode == .detail ? .squareGrid3x3 : .rectangleGrid1x2, + ) + } + .disabled(monthsLoad == nil || zoom.phase != nil) } - .disabled(monthsLoad == nil || zoom.phase != nil) } if report.showsEstimatedTimeAndPlanning { ToolbarItem(placement: .topBarTrailing) { @@ -133,7 +148,7 @@ struct CalendarContentView: View { String(localized: .plannedStaysTitle), systemSymbol: .calendarBadgeClock, ) { - planningDestination = .list + navigation.planningDestination = .list } } } @@ -181,14 +196,20 @@ struct CalendarContentView: View { } private func calendarContent(months: [CalendarMonth]) -> some View { - let visibleMonths = CalendarPresentationModel.visibleMonths( + @Bindable var navigation = navigation + let availableMonths = CalendarPresentationModel.visibleMonths( months, report: report, focusedRegion: focusedRegion, ) let currentMonth = months.first(where: \.isCurrentMonth) let estimateMonth = months.first(where: showsForecast(after:)) - let overview = presentation.mode == .overview + let visibleMonths = displayMode == .year ? availableMonths : months.filter { + monthKey($0) == + (navigation.displayedMonth ?? currentMonth.map(monthKey) ?? availableMonths.last + .map(monthKey)) + } + let overview = displayMode == .year && presentation.mode == .overview return ScrollView { Group { @@ -221,12 +242,12 @@ struct CalendarContentView: View { } .id(target(for: month)) .accessibilityFocused($focusedTarget, equals: target(for: month)) - if showsForecast(after: month) { + if displayMode == .year, showsForecast(after: month) { LocationForecastPanel( forecasts: calendarForecasts, microprintRegions: report.ranking.primary.map(\.region), homeRegion: report.forecasts.planning.homeRegion, - planningAction: { planningDestination = .list }, + planningAction: { navigation.planningDestination = .list }, ) .anchorPreference(key: CalendarZoomFrames.self, value: .bounds) { [.detail: [.tile(.estimate(monthKey(month))): $0]] @@ -252,7 +273,7 @@ struct CalendarContentView: View { } .id(presentation.mode) .transition(stylesheet.calendar.overview.transition) - .scrollPosition($scrollPosition) + .scrollPosition($navigation.calendarScrollPosition) .onScrollGeometryChange(for: CGPoint.self) { CGPoint( x: $0.contentOffset.x + $0.contentInsets.leading, @@ -262,8 +283,11 @@ struct CalendarContentView: View { scrollOffset = value } .onScrollTargetVisibilityChange(idType: CalendarPresentationModel.Target.self) { targets in - if zoom.phase == nil, scrollPosition.isPositionedByUser, - let first = targets.first { presentation.remember(first) } + if zoom.phase == nil, navigation.calendarScrollPosition.isPositionedByUser, + let first = targets.first + { presentation.remember(first) + navigation.recordVisibleMonths([first.month]) + } } .simultaneousGesture(MagnifyGesture().onEnded { value in let viewportSize = zoom.measurement?.viewport.size ?? .zero @@ -275,6 +299,7 @@ struct CalendarContentView: View { .first(where: { $0.key.month.year == report.selectedYear && $0.value.contains(point) })?.key + guard displayMode == .year else { return } changeZoom { presentation.finishPinch( magnification: value.magnification, @@ -290,29 +315,42 @@ struct CalendarContentView: View { .task(id: CalendarLayoutID( year: report.selectedYear, mode: presentation.mode, - availableMonths: visibleMonths.map(monthKey), + availableMonths: availableMonths.map(monthKey), estimateMonth: estimateMonth.map(monthKey), )) { guard !Task.isCancelled else { return } - let isInitial = presentation.year == nil + let isInitial = navigation.initiallyPositionedYear != report.selectedYear + navigation.restorePosition( + in: report.selectedYear, + initialMonth: (currentMonth ?? availableMonths.last).map(monthKey), + ) presentation.reconcile( year: report.selectedYear, - availableMonths: visibleMonths.map(monthKey), + availableMonths: availableMonths.map(monthKey), currentMonth: currentMonth.map(monthKey), estimateMonth: estimateMonth.map(monthKey), ) - if !overview, let target = presentation.target { + if applyReveal() { return } + if displayMode == .year, !overview, + let target = presentation.target + { let initialTarget = isInitial ? estimateMonth .map { CalendarPresentationModel.Target.estimate(monthKey($0)) } ?? target : target if zoom.phase == nil { - scrollPosition.scrollTo(id: initialTarget, anchor: isInitial ? .bottom : .top) + navigation.calendarScrollPosition.scrollTo( + id: initialTarget, + anchor: isInitial ? .bottom : .top, + ) if !isInitial { focusedTarget = target } } } else { - scrollPosition.scrollTo(edge: .top) + navigation.calendarScrollPosition.scrollTo(edge: .top) } } + .onChange(of: navigation.revealRequest, initial: true) { _, _ in + _ = applyReveal() + } .overlayPreferenceValue(CalendarZoomFrames.self) { anchors in GeometryReader { proxy in let measured = CalendarZoomFrames.measure( @@ -328,7 +366,7 @@ struct CalendarContentView: View { let value, value.mode == .detail, let target = presentation.target, value.frames[target] != nil { - scrollPosition.scrollTo(id: target, anchor: .top) + navigation.calendarScrollPosition.scrollTo(id: target, anchor: .top) } } } @@ -349,6 +387,23 @@ struct CalendarContentView: View { } } + @discardableResult + private func applyReveal() -> Bool { + guard let request = navigation.pendingReveal, + request.month.year == report.selectedYear, + presentation.year == report.selectedYear, + displayMode == .month || presentation.isAvailable(.month(request.month)) + else { return false } + zoom.cancel() + if displayMode == .year { presentation.expand(.month(request.month)) } + navigation.calendarScrollPosition.scrollTo( + id: CalendarPresentationModel.Target.month(request.month), + anchor: .top, + ) + navigation.consumeReveal(request) + return true + } + private struct CalendarLayoutID: Equatable { let year: Int let mode: CalendarPresentationModel.Mode @@ -376,16 +431,18 @@ struct CalendarContentView: View { zoom.updateMeasurement(nil) switch presentation.mode { case .overview: - scrollPosition = ScrollPosition( + navigation.calendarScrollPosition = ScrollPosition( idType: CalendarPresentationModel.Target.self, edge: .top, ) case .detail: if zoom.phase != nil { - scrollPosition = ScrollPosition(idType: CalendarPresentationModel.Target - .self) + navigation + .calendarScrollPosition = + ScrollPosition(idType: CalendarPresentationModel.Target + .self) } else if let target = presentation.target { - scrollPosition = ScrollPosition(id: target, anchor: .top) + navigation.calendarScrollPosition = ScrollPosition(id: target, anchor: .top) } } } @@ -411,7 +468,8 @@ struct CalendarContentView: View { forecasts: calendarForecasts, microprintRegions: report.ranking.primary.map(\.region), homeRegion: report.forecasts.planning.homeRegion, - planningAction: mode == .detail ? { planningDestination = .list } : nil, + planningAction: mode == .detail ? { navigation.planningDestination = .list } : + nil, ) } } @@ -438,6 +496,11 @@ struct CalendarContentView: View { .plannedRegionSummaries(in: month) : [], compact: compact, isAvailable: isAvailable, + displayMode: displayMode, + selectedInterval: history?.selectedInterval(in: report.selectedYear), + selectMonth: compact ? nil : history.map { history in + { history.select(month: monthKey(month), dateCalendar: report.calendar) } + }, ) } @@ -506,6 +569,10 @@ private struct MonthGridView: View { var compact = false var isAvailable = true + var displayMode: CalendarContentView.Presentation = .year + var selectedInterval: ClosedRange? + var selectMonth: (() -> Void)? + @Environment(\.stylesheet) private var stylesheet private var calendar: WhereStylesheet.CalendarStyle { @@ -548,17 +615,72 @@ private struct MonthGridView: View { var body: some View { let monthAccessibility = accessibility - VStack(alignment: .leading, spacing: calendar.month.sectionSpacing) { - Text(monthName) - .font(compact ? .system(size: calendar.overview.titleFontSize, weight: .semibold) : - .title.weight(.semibold)) - .frame(maxWidth: .infinity, alignment: .leading) - .foregroundStyle(isAvailable ? card.foreground : calendar.overview - .unavailableForeground) + Group { + if displayMode == .month { + ViewThatFits(in: .horizontal) { + HStack(alignment: .top, spacing: calendar.month.summarySpacing) { + VStack(alignment: .leading, spacing: calendar.month.sectionSpacing) { + monthHeading; monthSummary + } + .frame(width: calendar.month.summaryWidth) + dayGrid.frame(minWidth: calendar.month.minimumGridWidth) + } + VStack(alignment: .leading, spacing: calendar.month.sectionSpacing) { + monthHeading; dayGrid; monthSummary + } + } + } else { + VStack(alignment: .leading, spacing: calendar.month.sectionSpacing) { + monthHeading; dayGrid; monthSummary + } + } + } + .padding(calendar.month.padding) + .foregroundStyle(card.foreground) + .background { + // Transparent month cards share the screen background. The current + // month stands out through its accent text and heavier border. + cardShape + .fill(card.fill) + .overlay { + cardShape.strokeBorder(card.border, lineWidth: card.borderWidth) + } + } + .contentShape(cardShape) + .accessibilityRepresentation { + if let selectMonth { + Button(monthAccessibility.label, action: selectMonth) + .accessibilityValue(monthAccessibility.value) + } else { + Text(monthAccessibility.label).accessibilityValue(monthAccessibility.value) + } + } + } - dayGrid + private var monthHeading: some View { + Group { + if let selectMonth { Button(monthName, action: selectMonth).buttonStyle(.plain) } + else { Text(monthName) } + } + .font(compact ? .system(size: calendar.overview.titleFontSize, weight: .semibold) : + .title.weight(.semibold)) + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundStyle(isAvailable ? card.foreground : calendar.overview + .unavailableForeground) + } - if !compact, !month.regionTotals.isEmpty { + private var monthSummary: some View { + VStack(alignment: .leading, spacing: calendar.month.sectionSpacing) { + if displayMode == .month, let focusedRegion { + MonthFooter( + totals: [month.regionTotals + .first { $0.region == focusedRegion } ?? RegionDayTally( + region: focusedRegion, + days: 0, + )], + focusedRegion: focusedRegion, + ) + } else if !compact, !month.regionTotals.isEmpty { MonthFooter(totals: month.regionTotals, focusedRegion: focusedRegion) } ForEach( @@ -589,21 +711,6 @@ private struct MonthGridView: View { .foregroundStyle(.secondary) } } - .padding(calendar.month.padding) - .foregroundStyle(card.foreground) - .background { - // Transparent month cards share the screen background. The current - // month stands out through its accent text and heavier border. - cardShape - .fill(card.fill) - .overlay { - cardShape.strokeBorder(card.border, lineWidth: card.borderWidth) - } - } - .contentShape(cardShape) - .accessibilityElement(children: .ignore) - .accessibilityLabel(monthAccessibility.label) - .accessibilityValue(monthAccessibility.value) } /// A month has at most six weeks. Eager rows keep its intrinsic height exact @@ -614,7 +721,8 @@ private struct MonthGridView: View { .weekdayCount return Grid( horizontalSpacing: calendar.month.gridSpacing, - verticalSpacing: calendar.month.gridSpacing, + verticalSpacing: displayMode == .month ? calendar.month.tabletopGridSpacing : calendar + .month.gridSpacing, ) { GridRow { ForEach(Array(weekdaySymbols.enumerated()), id: \.offset) { _, symbol in @@ -637,6 +745,10 @@ private struct MonthGridView: View { compact: compact, isAvailable: isAvailable, ) + .historySelection(selectedInterval?.contains(CalendarDay( + from: month.days[index].date, + in: dateCalendar, + )) == true) } else { Color.clear .frame(height: calendar.day.minHeight) @@ -1032,6 +1144,15 @@ private struct DayCell: View { #if DEBUG extension CalendarContentView: SnapshotProviding { static var snapshots: [SnapshotCase] { + whereSnapshot( + name: "HistoryColumn", + configurations: [SnapshotConfiguration(device: AdaptiveHistorySnapshotFixture + .historyColumn)], + ) { + NavigationStack { + CalendarContentView(report: PreviewSupport.loadedYearReportModel()) + } + } whereSnapshot( name: "FlightAndEvidenceBadges", configurations: .componentRepresentative, diff --git a/Where/WhereUI/Sources/Primary/HistoryMonthControls.swift b/Where/WhereUI/Sources/Primary/HistoryMonthControls.swift new file mode 100644 index 000000000..d8b507336 --- /dev/null +++ b/Where/WhereUI/Sources/Primary/HistoryMonthControls.swift @@ -0,0 +1,76 @@ +import SFSafeSymbols +import SwiftUI +import WhereCore + +/// Explicit month browsing controls shared by both tabletop destinations. +struct HistoryMonthControls: View { + let report: YearReportModel + let history: HistoryNavigationModel + @Environment(\.stylesheet) private var stylesheet + + private var month: CalendarDay { + if let month = history.calendar.displayedMonth, month.year == report.selectedYear { + return month + } + let today = CalendarDay(from: report.referenceDate, in: report.calendar) + return CalendarDay( + year: report.selectedYear, + month: today.year == report.selectedYear ? today.month : 12, + day: 1, + ) + } + + var body: some View { + HStack { + Button(String(localized: .adaptiveMonthPrevious), systemSymbol: .chevronBackward) { + select(month.month - 1) + } + .disabled(month.month == 1) + .labelStyle(.iconOnly) + Spacer(minLength: stylesheet.spacing.small) + Menu { + ForEach(1 ... 12, id: \.self) { candidate in + Button(title(for: candidate)) { select(candidate) } + } + } label: { + Text(title(for: month.month)) + .font(.headline) + } + .accessibilityLabel(String(localized: .adaptiveMonthPicker)) + .accessibilityValue(title(for: month.month)) + Spacer(minLength: stylesheet.spacing.small) + Button(String(localized: .adaptiveMonthNext), systemSymbol: .chevronForward) { + select(month.month + 1) + } + .disabled(month.month == 12) + .labelStyle(.iconOnly) + } + .padding(stylesheet.spacing.medium) + .background(.bar) + } + + private func title(for month: Int) -> String { + CalendarDay(year: report.selectedYear, month: month, day: 1) + .startOfDay(in: report.calendar).formatted(Date.FormatStyle( + calendar: report.calendar, + timeZone: report.calendar.timeZone, + ).month(.wide).year()) + } + + private func select(_ month: Int) { + history.select( + month: CalendarDay(year: report.selectedYear, month: month, day: 1), + dateCalendar: report.calendar, + ) + } +} + +#if DEBUG + #Preview { + HistoryMonthControls( + report: PreviewSupport.loadedYearReportModel(), + history: HistoryNavigationModel(), + ) + .whereBroadwayRoot() + } +#endif diff --git a/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift b/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift index 8292ac616..8b4912068 100644 --- a/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift +++ b/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift @@ -22,7 +22,11 @@ struct LocationWelcomeOverlay: View { .transition(.opacity) .zIndex(0) - GeometryReader { proxy in + AdaptiveHistoryGeometryReader { geometry in + let area = geometry.unobstructedBounds(near: CGPoint( + x: geometry.contentBounds.midX, + y: geometry.contentBounds.maxY, + )) ScrollView { RegionWelcomeCard( presentation: presentation, @@ -32,10 +36,12 @@ struct LocationWelcomeOverlay: View { .fixedSize(horizontal: false, vertical: true) .padding(.horizontal, stylesheet.spacing.xxxLarge) .padding(.vertical, stylesheet.spacing.xxxLarge) - .frame(maxWidth: .infinity, minHeight: proxy.size.height) + .frame(maxWidth: .infinity, minHeight: area.height) .accessibilityFocused($isCardFocused) } .scrollBounceBehavior(.basedOnSize) + .frame(width: area.width, height: area.height) + .position(x: area.midX, y: area.midY) } .contentShape(Rectangle()) .accessibilityAddTraits(.isModal) diff --git a/Where/WhereUI/Sources/Primary/LocationsCardsView.swift b/Where/WhereUI/Sources/Primary/LocationsCardsView.swift index fab7e938d..223067288 100644 --- a/Where/WhereUI/Sources/Primary/LocationsCardsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsCardsView.swift @@ -12,6 +12,8 @@ struct LocationsCardsView: View { let isCardSurfaceUncovered: Bool @Binding var isCardSurfaceVisible: Bool let planningAction: () -> Void + @Bindable var navigation: SceneNavigationModel.Locations + let cardVariant: WhereStylesheet.CardStyle.Variant @Environment(\.stylesheet) private var stylesheet @Environment(\.regionStyles) private var regionStyles @@ -23,7 +25,7 @@ struct LocationsCardsView: View { // a short card list retains its centered presentation. return ScrollView { VStack(spacing: stylesheet.spacing.xxLarge) { - LocationsFlightNotice(report: report) + LocationsFlightNotice(report: report, openReview: { navigation.open(.flight($0)) }) LocationCardRankingStack( spacing: stylesheet.spacing.xxLarge, @@ -31,11 +33,12 @@ struct LocationsCardsView: View { motion: stylesheet.locationCardStack.overtake, ) { ForEach(presentedCards) { item in - NavigationLink { - calendarDestination(item.region) + Button { + navigation.select(region: item.region) } label: { RegionSummaryCard( regionDays: item, + variant: cardVariant, interactive: true, yearLength: report.daysInSelectedYear, estimatedDays: estimatedDays(for: item.region), @@ -47,7 +50,7 @@ struct LocationsCardsView: View { recordedPointsID: report.primaryRegionLocations?.id, ) } - .buttonStyle(stylesheet.card.regular.buttonStyle( + .buttonStyle(stylesheet.card[cardVariant].buttonStyle( tint: regionStyles.style(for: item.region).tint, )) // The card is the zoom source: tapping it expands the @@ -60,7 +63,7 @@ struct LocationsCardsView: View { id: item.region, in: calendarTransition, ) { source in - let card = stylesheet.card.regular + let card = stylesheet.card[cardVariant] let tint = regionStyles.style(for: item.region).tint let elevation = card.transitionElevation(tint: tint) return source @@ -87,21 +90,34 @@ struct LocationsCardsView: View { motion: stylesheet.locationCardStack.overtake, ) .locationCardRankingRegion(item.region) + .id(item.region) + .historySelection( + navigation.selectedRegion == item.region, + cornerRadius: stylesheet.card[cardVariant].cornerRadius, + ) } } + .scrollTargetLayout() + LocationsEstimatePanel(report: report, planningAction: planningAction) } .padding() .frame(maxWidth: .infinity) } - .defaultScrollAnchor(report.liveFlightReview == nil ? .center : .top) + .defaultScrollAnchor(.top, for: .initialOffset) + .defaultScrollAnchor(report.liveFlightReview == nil ? .center : .top, for: .alignment) + .scrollPosition($navigation.scrollPosition) + .onScrollTargetVisibilityChange(idType: Region.self, threshold: 0.01) { regions in + guard navigation.scrollPosition.isPositionedByUser else { return } + navigation.visibleRegion = presentedCards.first { regions.contains($0.region) }?.region + } .scrollBounceBehavior(.basedOnSize) // The card normally stays clipped to the scrolling viewport. Reveal // overflow only while the authored arc, scale, and rotation need it. .scrollClipDisabled(cardPresentation.isSpatialOvertakeActive) .accessibilityIdentifier("where_root_title") - .onAppear { isCardSurfaceVisible = true } + .onAppear { isCardSurfaceVisible = true; navigation.restoreBrowsePosition() } .onDisappear { isCardSurfaceVisible = false } // Count, order, flourish, persistence, and haptic all share this one // visibility-aware delayed reconciliation. @@ -118,18 +134,6 @@ struct LocationsCardsView: View { guard report.showsEstimatedTimeAndPlanning else { return nil } return report.forecasts.forecast(for: region, report: report.report)?.estimatedTotalDays } - - /// The region's calendar, pushed as a nested view. It's the zoom - /// destination: the tapped card expands into it via matched geometry, and the - /// stack's back gesture collapses it again. - private func calendarDestination(_ region: Region) -> some View { - CalendarContentView(focusedRegion: region, report: report, persistenceKey: .region(region)) - .navigationTitle( - WhereFormat.calendarRegionTitle(region: region, year: report.selectedYear), - ) - .navigationBarTitleDisplayMode(.inline) - .navigationTransition(.zoom(sourceID: region, in: calendarTransition)) - } } #if DEBUG diff --git a/Where/WhereUI/Sources/Primary/LocationsFlightNotice.swift b/Where/WhereUI/Sources/Primary/LocationsFlightNotice.swift index d21e74bec..d8e97dfac 100644 --- a/Where/WhereUI/Sources/Primary/LocationsFlightNotice.swift +++ b/Where/WhereUI/Sources/Primary/LocationsFlightNotice.swift @@ -1,20 +1,26 @@ import SwiftUI +import WhereCore /// Keeps the flight link and its destination behind one small value boundary. struct LocationsFlightNotice: View { let report: YearReportModel + var openReview: ((GPSCorrectionReview) -> Void)? var body: some View { if let review = report.liveFlightReview { - NavigationLink { - FlightDayDetailView(review: review, report: report) - } label: { - FlightStatusBanner( - review: review, - deviceLabel: report.liveFlightAssessment(in: review) - .map(report.flightDeviceLabel), - flight: report.liveFlightAssessment(in: review), - ) + let banner = FlightStatusBanner( + review: review, + deviceLabel: report.liveFlightAssessment(in: review).map(report.flightDeviceLabel), + flight: report.liveFlightAssessment(in: review), + ) + Group { + if let openReview { + Button { openReview(review) } label: { banner } + } else { + NavigationLink { FlightDayDetailView(review: review, report: report) } label: { + banner + } + } } .buttonStyle(.plain) .accessibilityHint(String(localized: .flightStatusOpenReview)) diff --git a/Where/WhereUI/Sources/Primary/LocationsStateView.swift b/Where/WhereUI/Sources/Primary/LocationsStateView.swift index 4d2e897f6..42b3505a8 100644 --- a/Where/WhereUI/Sources/Primary/LocationsStateView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsStateView.swift @@ -1,10 +1,12 @@ import SwiftUI +import WhereCore /// Bounds loading, failure, and empty-state construction separately from the /// navigation shell. Keep the state payload behind its own view boundary. struct LocationsStateView: View { let report: YearReportModel let planningAction: () -> Void + var openReview: ((GPSCorrectionReview) -> Void)? var openElsewhere: (() -> Void)? @Environment(\.stylesheet.spacing) private var spacing @@ -12,7 +14,7 @@ struct LocationsStateView: View { var body: some View { ScrollView { VStack(spacing: spacing.xxLarge) { - LocationsFlightNotice(report: report) + LocationsFlightNotice(report: report, openReview: openReview) LocationsStateContent( report: report, planningAction: planningAction, diff --git a/Where/WhereUI/Sources/Primary/LocationsView.swift b/Where/WhereUI/Sources/Primary/LocationsView.swift index cdd2aae5b..d532ae9fd 100644 --- a/Where/WhereUI/Sources/Primary/LocationsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsView.swift @@ -6,14 +6,213 @@ import SnapshotKit import SwiftUI import WhereCore -/// Locations tab: configured primary regions with recorded days in the selected -/// year, ranked by day count, plus planning and data-resolution actions. +/// Stable destination ownership around native browse/detail and current-history tabletop layouts. struct LocationsView: View { let report: YearReportModel var openElsewhere: (() -> Void)? + @State private var navigation: SceneNavigationModel.Locations + @State private var cardPresentation: LocationCardsPresentationModel + @State private var tilt = TiltProvider() + @Namespace private var calendarTransition + @Environment(\.stylesheet) private var stylesheet + + init( + report: YearReportModel, + navigation: SceneNavigationModel.Locations? = nil, + openElsewhere: (() -> Void)? = nil, + ) { + self.report = report + self.openElsewhere = openElsewhere + _navigation = State(initialValue: navigation ?? SceneNavigationModel.Locations()) + _cardPresentation = State(initialValue: LocationCardsPresentationModel( + preferences: report.preferences, + year: report.selectedYear, + )) + } + + var body: some View { + @Bindable var navigation = navigation + AdaptiveHistoryContainer { arrangement in + if let panes = tabletopPanes(for: arrangement) { + TabletopHistoryLayout(panes: panes) { + calendar(presentation: .month, usesZoomTransition: false) + } controls: { + NavigationStack { + browseContent + .safeAreaInset(edge: .top, spacing: 0) { + HistoryMonthControls(report: report, history: navigation.history) + } + } + } + } else { + NavigationSplitView( + columnVisibility: .constant(.all), + preferredCompactColumn: $navigation.compactColumn, + ) { + LocationsBrowsePane( + report: report, + navigation: navigation, + cardPresentation: cardPresentation, + tilt: tilt, + calendarTransition: calendarTransition, + cardVariant: arrangement == .compact ? .regular : .column, + openElsewhere: openElsewhere, + ) + .navigationSplitViewColumnWidth( + min: stylesheet.adaptiveHistory.minimumControlsWidth, + ideal: stylesheet.adaptiveHistory.minimumControlsWidth, + ) + } detail: { + calendar( + presentation: .year, + usesZoomTransition: arrangement == .compact, + ) + .navigationSplitViewColumnWidth( + min: stylesheet.adaptiveHistory.minimumHistoryWidth, + ideal: stylesheet.adaptiveHistory.minimumHistoryWidth, + ) + } + .navigationSplitViewStyle(.balanced) + .environment(\.horizontalSizeClass, arrangement == .compact ? .compact : .regular) + .bHorizontalSizeClass(arrangement == .compact ? .compact : .regular) + } + } + .sheet(isPresented: $navigation.showingResolution) { + ResolutionView(report: report) + } + .modifier(PlanningPresentation(report: report, navigation: navigation.history.planning)) + } + + private var browseContent: some View { + LocationsContent( + report: report, + navigation: navigation, + cardPresentation: cardPresentation, + tilt: tilt, + calendarTransition: calendarTransition, + openElsewhere: openElsewhere, + ) + } + + /// Drill-ins keep their native stack mounted while their local editors are open. + /// The custom tabletop composition belongs to the top-level history surface. + private func tabletopPanes(for arrangement: AdaptiveHistoryArrangement) + -> AdaptiveHistoryArrangement.Panes? + { + guard navigation.browsePath.isEmpty, + case let .tabletop(panes) = arrangement else { return nil } + return panes + } - @State private var showingResolution = false - @State private var planningDestination: PlannedStaysDestination? + private func calendar( + presentation: CalendarContentView.Presentation, + usesZoomTransition: Bool, + ) -> some View { + @Bindable var navigation = navigation + return NavigationStack(path: $navigation.detailPath) { + CalendarContentView( + focusedRegion: navigation.selectedRegion, + report: report, + history: navigation.history, + presentation: presentation, + persistenceKey: navigation.selectedRegion.map(CalendarZoomKey.region), + ) + .id(navigation.selectedRegion) + .navigationTitle(navigation.selectedRegion.map { + WhereFormat.calendarRegionTitle(region: $0, year: report.selectedYear) + } ?? String(localized: .primaryCalendar)) + .navigationBarTitleDisplayMode(.inline) + } + .modifier(RegionCalendarTransition( + region: usesZoomTransition ? navigation.selectedRegion : nil, + namespace: calendarTransition, + )) + } +} + +/// Native split navigation uses the same card zoom when its detail collapses to a push. +private struct RegionCalendarTransition: ViewModifier { + let region: Region? + let namespace: Namespace.ID + + func body(content: Content) -> some View { + if let region { + content.navigationTransition(.zoom(sourceID: region, in: namespace)) + } else { + content + } + } +} + +/// The stable browse stack. Pushed flows retain their native navigation and draft state. +struct LocationsBrowsePane: View { + let report: YearReportModel + var openElsewhere: (() -> Void)? + @State private var navigation: SceneNavigationModel.Locations + @State private var cardPresentation: LocationCardsPresentationModel + @State private var tilt: TiltProvider + @Namespace private var localTransition + private let transition: Namespace.ID? + let cardVariant: WhereStylesheet.CardStyle.Variant + + init( + report: YearReportModel, + navigation: SceneNavigationModel.Locations? = nil, + cardPresentation: LocationCardsPresentationModel? = nil, + tilt: TiltProvider? = nil, + calendarTransition: Namespace.ID? = nil, + cardVariant: WhereStylesheet.CardStyle.Variant = .regular, + openElsewhere: (() -> Void)? = nil, + ) { + self.report = report + self.openElsewhere = openElsewhere + _navigation = State(initialValue: navigation ?? SceneNavigationModel.Locations()) + _cardPresentation = State(initialValue: cardPresentation ?? LocationCardsPresentationModel( + preferences: report.preferences, + year: report.selectedYear, + )) + _tilt = State(initialValue: tilt ?? TiltProvider()) + transition = calendarTransition + self.cardVariant = cardVariant + } + + var body: some View { + @Bindable var navigation = navigation + NavigationStack(path: $navigation.browsePath) { + LocationsContent( + report: report, + navigation: navigation, + cardPresentation: cardPresentation, + tilt: tilt, + calendarTransition: transition ?? localTransition, + cardVariant: cardVariant, + openElsewhere: openElsewhere, + ) + .navigationDestination(for: SceneNavigationModel.Locations.Destination + .self) + { destination in + switch destination { + case .elsewhere: ElsewhereView(report: report) + case let .flight(review): FlightDayDetailView( + review: review, + report: report, + ) + } + } + } + } +} + +/// Locations tab: the regions you spend the most days in for the selected year, +/// shown as prominent Liquid Glass cards, with the Elsewhere summary folded in +/// at the bottom (only when there are secondary regions), plus toolbar actions +/// for planned stays and any data issues that need resolution. +struct LocationsContent: View { + let report: YearReportModel + var openElsewhere: (() -> Void)? + let cardVariant: WhereStylesheet.CardStyle.Variant + + @State private var navigation: SceneNavigationModel.Locations @State private var isCardSurfaceVisible = false @State private var cardPresentation: LocationCardsPresentationModel @@ -24,72 +223,76 @@ struct LocationsView: View { /// Pairs a tapped card with its pushed calendar so the navigation uses a /// matched-geometry zoom (the card expands into the calendar) rather than a /// plain slide. - @Namespace private var calendarTransition + @Namespace private var localCalendarTransition + private let transition: Namespace.ID? + private var calendarTransition: Namespace.ID { + transition ?? localCalendarTransition + } @Environment(\.stylesheet.card.regular.surface) private var cardSurface private var isCardSurfaceUncovered: Bool { isCardSurfaceVisible - && !showingResolution - && planningDestination == nil + && !navigation.showingResolution + && navigation.planningDestination == nil } - init(report: YearReportModel, openElsewhere: (() -> Void)? = nil) { + init( + report: YearReportModel, + navigation: SceneNavigationModel.Locations? = nil, + cardPresentation: LocationCardsPresentationModel? = nil, + tilt: TiltProvider? = nil, + calendarTransition: Namespace.ID? = nil, + cardVariant: WhereStylesheet.CardStyle.Variant = .regular, + openElsewhere: (() -> Void)? = nil, + ) { + _navigation = State(initialValue: navigation ?? SceneNavigationModel.Locations()) self.report = report self.openElsewhere = openElsewhere - _cardPresentation = State(initialValue: LocationCardsPresentationModel( + self.cardVariant = cardVariant + transition = calendarTransition + _tilt = State(initialValue: tilt ?? TiltProvider()) + _cardPresentation = State(initialValue: cardPresentation ?? LocationCardsPresentationModel( preferences: report.preferences, year: report.selectedYear, )) } var body: some View { - NavigationStack { - screen - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background { - LocationsBackground(regions: backgroundRegions) - .ignoresSafeArea() - } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItemGroup(placement: .topBarTrailing) { - // Pending reviews remain accessible without increasing - // the actionable issue badge. - if report.dataIssueCount > 0 || report.hasCorrectionReviews { - Button { - showingResolution = true - } label: { - ResolveToolbarLabel(count: report.dataIssueCount) - } - .accessibilityIdentifier("where_resolution_button") + @Bindable var navigation = navigation + screen + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background { + LocationsBackground(regions: backgroundRegions) + .ignoresSafeArea() + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + // Pending reviews remain accessible without increasing + // the actionable issue badge. + if report.dataIssueCount > 0 || report.hasCorrectionReviews { + Button { + navigation.showingResolution = true + } label: { + ResolveToolbarLabel(count: report.dataIssueCount) } + .accessibilityIdentifier("where_resolution_button") + } - if showsPlanningMenu { - Button( - String(localized: .plannedStaysTitle), - systemSymbol: .calendarBadgeClock, - ) { - planningDestination = .list - } - .accessibilityIdentifier("where_planning_menu") + if showsPlanningMenu { + Button( + String(localized: .plannedStaysTitle), + systemSymbol: .calendarBadgeClock, + ) { + navigation.planningDestination = .list } + .accessibilityIdentifier("where_planning_menu") } } - } - .observesTilt( - tilt, - enabled: isCardSurfaceUncovered && cardSurface.usesReflection, - ) - .sheet(isPresented: $showingResolution) { - ResolutionView(report: report) - } - .sheet(item: $planningDestination) { destination in - PlannedStaysDestinationView(destination: destination, report: report) - } - // Log View Mode: reveal an inspect badge for the year-report events - // backing this screen. A no-op in release. - .debugLogInspectable(WhereLog.session(YearReportModelLog.self)) + } + .observesTilt(tilt, enabled: isCardSurfaceUncovered && cardSurface.usesReflection) + .debugLogInspectable(WhereLog.session(YearReportModelLog.self)) } @ViewBuilder @@ -99,6 +302,7 @@ struct LocationsView: View { LocationsStateView( report: report, planningAction: openPlanning, + openReview: { navigation.open(.flight($0)) }, openElsewhere: openElsewhere, ) case .idle, .loaded, .loading: @@ -106,6 +310,7 @@ struct LocationsView: View { LocationsStateView( report: report, planningAction: openPlanning, + openReview: { navigation.open(.flight($0)) }, openElsewhere: openElsewhere, ) } else { @@ -117,6 +322,8 @@ struct LocationsView: View { isCardSurfaceUncovered: isCardSurfaceUncovered, isCardSurfaceVisible: $isCardSurfaceVisible, planningAction: openPlanning, + navigation: navigation, + cardVariant: cardVariant, ) } } @@ -136,7 +343,7 @@ struct LocationsView: View { } private func openPlanning() { - planningDestination = .list + navigation.planningDestination = .list } } @@ -185,6 +392,13 @@ private struct ResolveToolbarLabel: View { reduceTransparency: true, ) } + artworkSnapshot( + name: "BrowseColumn", + report: PreviewSupport.loadedYearReportModel(), + configurations: [SnapshotConfiguration(device: AdaptiveHistorySnapshotFixture + .browseColumn)], + cardVariant: .column, + ) artworkSnapshot( name: "Itinerary", report: PreviewSupport.itineraryYearReportModel(), @@ -208,7 +422,7 @@ private struct ResolveToolbarLabel: View { measurementReadiness: .immediate, settle: .settledAtLeast(minDuration: 1.0), ) { - LocationsView(report: PreviewSupport.plannedStayYearReportModel()) + LocationsBrowsePane(report: PreviewSupport.plannedStayYearReportModel()) } whereSnapshot( name: "ForecastsHidden", @@ -216,7 +430,7 @@ private struct ResolveToolbarLabel: View { measurementReadiness: .immediate, settle: .settledAtLeast(minDuration: 1.0), ) { - LocationsView(report: forecastsHiddenReport()) + LocationsBrowsePane(report: forecastsHiddenReport()) } whereSnapshot( name: "Empty", @@ -224,7 +438,7 @@ private struct ResolveToolbarLabel: View { measurementReadiness: .immediate, settle: .settledAtLeast(minDuration: 1.0), ) { - LocationsView(report: PreviewSupport.emptyYearReportModel()) + LocationsBrowsePane(report: PreviewSupport.emptyYearReportModel()) } whereSnapshot( name: "MissingDays", @@ -232,7 +446,7 @@ private struct ResolveToolbarLabel: View { measurementReadiness: .immediate, settle: .settledAtLeast(minDuration: 1.0), ) { - LocationsView(report: PreviewSupport.missingDaysYearReportModel()) + LocationsBrowsePane(report: PreviewSupport.missingDaysYearReportModel()) } whereSnapshot( name: "ElsewhereOnly", @@ -260,7 +474,7 @@ private struct ResolveToolbarLabel: View { measurementReadiness: .immediate, settle: .settledAtLeast(minDuration: 1.0), ) { - LocationsView( + LocationsBrowsePane( report: PreviewSupport.loadedYearReportModelWithLocationDotsHidden(), ) } @@ -281,6 +495,8 @@ private struct ResolveToolbarLabel: View { configurations: [SnapshotConfiguration], theme: WhereTheme = .standard, reduceTransparency: Bool = false, + cardVariant: WhereStylesheet.CardStyle.Variant = .regular, + openElsewhere _: (() -> Void)? = nil, ) -> SnapshotCase { let cache = RegionOutlinePathCache() let regions = LocationsBackgroundArtwork.regions(in: report.ranking) @@ -300,7 +516,7 @@ private struct ResolveToolbarLabel: View { } }, ) { - LocationsView(report: report) + LocationsBrowsePane(report: report, cardVariant: cardVariant) .environment(\.regionOutlinePathCache, cache) .bTraitOverrides { traits, overrides in if reduceTransparency { diff --git a/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift b/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift index b74dea145..b0e6ca59f 100644 --- a/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift +++ b/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift @@ -15,16 +15,30 @@ import WhereCore struct PresenceTimelineList: View { enum Presentation { case full + case linked case excerpt } let report: YearReportModel - var presentation: Presentation = .full + let presentation: Presentation + private let ownsPlanningPresentation: Bool @Environment(\.stylesheet) private var stylesheet - @State private var planningDestination: PlannedStaysDestination? + @State private var history: HistoryNavigationModel + + init( + report: YearReportModel, + presentation: Presentation = .full, + history: HistoryNavigationModel? = nil, + ) { + self.report = report + self.presentation = presentation + ownsPlanningPresentation = history == nil + _history = State(initialValue: history ?? HistoryNavigationModel()) + } var body: some View { + @Bindable var navigation = history.timeline let yearReport = report.report let stints = yearReport .map { PresenceTimeline.stints(from: $0, calendar: report.calendar) } ?? [] @@ -41,6 +55,8 @@ struct PresenceTimelineList: View { today: report.forecasts.today, ) : [] + let rowIDs = recordedItems.map(TimelineNavigationModel.Item.init) + + plannedItems.map { .planned($0.id) } Group { if presentation == .excerpt { @@ -74,41 +90,10 @@ struct PresenceTimelineList: View { Text(String(localized: .timelineEmptyDescription)) } } else { - let pinsOverview = stylesheet.timeline.overview.pinsToViewport - - ScrollView { - // Measure the complete year before expanding a bottom-anchored viewport. - // Lazy estimates can leave blank space below the final forecast. - VStack(spacing: stylesheet.spacing.large) { - if !pinsOverview { - YearRibbon( - days: yearReport?.days ?? [], - year: report.selectedYear, - calendar: report.calendar, - ) - } + let pinsOverview = presentation == .full && stylesheet.timeline.overview + .pinsToViewport - VStack(spacing: 0) { - journeyRows(recordedItems: recordedItems, plannedItems: plannedItems) - } - - if showsForecast { - LocationForecastPanel( - forecasts: timelineForecasts, - microprintRegions: report.ranking.primary.map(\.region), - homeRegion: report.forecasts.planning.homeRegion, - planningAction: { planningDestination = .list }, - ) - } - } - .padding(.horizontal, stylesheet.spacing.xxLarge) - .padding(.top, pinsOverview ? 0 : stylesheet.spacing.large) - .padding(.bottom, stylesheet.spacing.large) - } - .safeAreaInset( - edge: .top, - spacing: 0, - ) { + VStack(spacing: 0) { if pinsOverview { YearRibbon( days: yearReport?.days ?? [], @@ -116,25 +101,107 @@ struct PresenceTimelineList: View { calendar: report.calendar, ) .padding(.horizontal, stylesheet.spacing.xxLarge) - .padding(.top, stylesheet.spacing.large) + .padding(.vertical, stylesheet.spacing.large) + } + ScrollView { + // Measure this year's overview, stays, and forecast together. + // Lazy row estimates can move the overview above the content + // origin when a bottom-anchored viewport expands. + VStack(spacing: stylesheet.spacing.large) { + if presentation == .full, !pinsOverview { + YearRibbon( + days: yearReport?.days ?? [], + year: report.selectedYear, + calendar: report.calendar, + ) + } + + VStack(spacing: 0) { + journeyRows( + recordedItems: recordedItems, + plannedItems: plannedItems, + ) + } + .scrollTargetLayout() + + if presentation == .full, showsForecast { + LocationForecastPanel( + forecasts: timelineForecasts, + microprintRegions: report.ranking.primary.map(\.region), + homeRegion: report.forecasts.planning.homeRegion, + planningAction: { history.planning.destination = .list }, + ) + } + } + .padding(.horizontal, stylesheet.spacing.xxLarge) + .padding(.top, pinsOverview ? 0 : stylesheet.spacing.large) .padding(.bottom, stylesheet.spacing.large) } + .scrollPosition($navigation.scrollPosition) + .onScrollTargetVisibilityChange( + idType: TimelineNavigationModel.Item.self, + threshold: 0.01, + ) { visible in + guard navigation.scrollPosition.isPositionedByUser else { return } + navigation.recordVisibleItems( + rowIDs.filter { visible.contains($0) }, + in: report.selectedYear, + ) + } + .task(id: report.selectedYear) { + navigation.restorePosition( + in: report.selectedYear, + initialItem: presentation == .linked + ? recordedItems.last.map(TimelineNavigationModel.Item.init) : nil, + ) + navigation.reconcile( + recordedItems: recordedItems, + plannedItems: plannedItems, + calendar: report.calendar, + ) + } + .onChange(of: recordedItems) { + navigation.reconcile( + recordedItems: recordedItems, + plannedItems: plannedItems, + calendar: report.calendar, + ) + } + .onChange(of: plannedItems) { + navigation.reconcile( + recordedItems: recordedItems, + plannedItems: plannedItems, + calendar: report.calendar, + ) + } + .onChange(of: navigation.monthRevealID) { + navigation.reconcile( + recordedItems: recordedItems, + plannedItems: plannedItems, + calendar: report.calendar, + ) + } + .defaultScrollAnchor( + presentation == .linked ? .top : .bottom, + for: .initialOffset, + ) + .id(report.selectedYear) } - .defaultScrollAnchor(.bottom, for: .initialOffset) - .id(report.selectedYear) } } - .sheet(item: $planningDestination) { destination in - PlannedStaysDestinationView(destination: destination, report: report) - } + .modifier(PlanningPresentation( + report: report, + navigation: history.planning, + isEnabled: ownsPlanningPresentation, + )) .toolbar { - if presentation == .full, report.showsEstimatedTimeAndPlanning { + if presentation != .excerpt, report.showsEstimatedTimeAndPlanning { ToolbarItem(placement: .topBarTrailing) { Button( String(localized: .plannedStaysTitle), systemSymbol: .calendarBadgeClock, ) { - planningDestination = .list + history.planning.destination = .list } } } @@ -156,26 +223,40 @@ struct PresenceTimelineList: View { Group { switch item { case let .stay(stint): - PresenceJourneyRow( + let row = PresenceJourneyRow( stint: stint, calendar: report.calendar, daysInYear: report.daysInSelectedYear, isFirst: item.id == recordedItems.first?.id, isLast: plannedItems.isEmpty && item.id == recordedItems.last?.id, - cardPosition: joinsRecorded && item.id == recordedItems.last? - .id ? .top : .standalone, + cardPosition: joinsRecorded && item.id == recordedItems.last?.id + ? .top : .standalone, ) + if presentation == .excerpt { + row + } else { + Button { + history.select(stint: stint, dateCalendar: report.calendar) + } label: { + row.historySelection(isSelected( + start: CalendarDay(from: stint.start, in: report.calendar), + end: CalendarDay(from: stint.end, in: report.calendar), + region: stint.region, + )) + } + .buttonStyle(.plain) + } case let .flight(flight): FlightJourneyRow( flight: flight, calendar: report.calendar, isFirst: item.id == recordedItems.first?.id, - isLast: plannedItems.isEmpty && item.id == recordedItems - .last?.id, + isLast: plannedItems.isEmpty && item.id == recordedItems.last?.id, ) } } .fixedSize(horizontal: false, vertical: presentation == .excerpt) + .id(TimelineNavigationModel.Item(item)) } ForEach(plannedItems) { item in let row = PlannedPresenceJourneyRow( @@ -189,26 +270,50 @@ struct PresenceTimelineList: View { ) .fixedSize(horizontal: false, vertical: presentation == .excerpt) - if presentation == .full { + if presentation != .excerpt { Button { - if case let .stay(interval) = item, - let stay = report.forecasts.planning.stays - .first(where: { $0.id == interval.stayID }) - { - planningDestination = .edit(stay) - } else { - planningDestination = .list - } + history.select(plannedItem: item) + if presentation == .full { edit(item) } } label: { - row + row.historySelection(isSelected( + start: item.start, + end: item.end, + region: item.region, + )) } .buttonStyle(.plain) + .contextMenu { + Button( + String(localized: .plannedStaysTitle), + systemSymbol: .calendarBadgeClock, + ) { + edit(item) + } + } + .id(TimelineNavigationModel.Item.planned(item.id)) } else { row } } } + private func edit(_ item: PlanningTimelineItem) { + if case let .stay(interval) = item, + let stay = report.forecasts.planning.stays.first(where: { $0.id == interval.stayID }) + { + history.planning.destination = .edit(stay) + } else { + history.planning.destination = .list + } + } + + private func isSelected(start: CalendarDay, end: CalendarDay, region: Region) -> Bool { + guard let selection = history.selection, + selection.region == nil || selection.region == region + else { return false } + return selection.interval.overlaps(start ... end) + } + private var showsForecast: Bool { report.showsEstimatedTimeAndPlanning && !timelineForecasts.isEmpty } @@ -233,6 +338,18 @@ struct PresenceTimelineList: View { .interruptedStayYearReportModel()) } }, + whereSnapshot( + name: "BrowseColumn", + configurations: [SnapshotConfiguration(device: AdaptiveHistorySnapshotFixture + .browseColumn)], + ) { + NavigationStack { + PresenceTimelineList( + report: PreviewSupport.loadedYearReportModel(), + presentation: .linked, + ) + } + }, whereSnapshot(name: "Flights", configurations: .fullContentScreenRepresentative) { NavigationStack { PresenceTimelineList(report: PreviewSupport diff --git a/Where/WhereUI/Sources/Primary/RegionWelcomeCard.swift b/Where/WhereUI/Sources/Primary/RegionWelcomeCard.swift index b98e63ebc..b110e13cd 100644 --- a/Where/WhereUI/Sources/Primary/RegionWelcomeCard.swift +++ b/Where/WhereUI/Sources/Primary/RegionWelcomeCard.swift @@ -1,5 +1,6 @@ import RegionKit import SFSafeSymbols +import SnapshotKit import SwiftUI /// A passport-inspired acknowledgement of the user's current tracked region. diff --git a/Where/WhereUI/Sources/RootView.swift b/Where/WhereUI/Sources/RootView.swift index f0896d4bd..b6fcd9111 100644 --- a/Where/WhereUI/Sources/RootView.swift +++ b/Where/WhereUI/Sources/RootView.swift @@ -29,7 +29,7 @@ public struct RootView: View { /// The logged-in tab bar's measured height, reported up from `MainTabs` and /// handed to the sibling `DeveloperOverlay` so its button rests clear of the /// tab bar. Zero when logged out (no tab bar in the tree). - @State private var developerTabBarInset: CGFloat = 0 + @State private var developerTabBarInset = EdgeInsets() /// The footprint the non-modal floating developer HUD occupies, reported up /// from the sibling `DeveloperOverlay` and applied as extra safe area to the /// app content so screens behind the HUD can scroll clear of it. diff --git a/Where/WhereUI/Sources/Settings/SettingsContent.swift b/Where/WhereUI/Sources/Settings/SettingsContent.swift new file mode 100644 index 000000000..0acacfb49 --- /dev/null +++ b/Where/WhereUI/Sources/Settings/SettingsContent.swift @@ -0,0 +1,255 @@ +import InstallerKit +import LifecycleKitUI +import RegionKit +import SFSafeSymbols +import SwiftUI +import WhereCore + +/// An embedded demo has no app lifecycle to exit. Its host owns dismissal. +enum DemoExitPresentation { + case session + case embedded +} + +extension EnvironmentValues { + @Entry var demoExitPresentation = DemoExitPresentation.session +} + +/// Shared searchable Settings list. Its host supplies navigation and sheet presentation. +struct SettingsContent: View { + let report: YearReportModel + let recordingWarning: RecordingConfigurationWarningModel + @State private var navigation: SceneNavigationModel.Settings + @Environment(UpdateModel.self) private var updates: UpdateModel? + @Environment(WhereSession.self) private var session + @Environment(WhereModel.self) private var model + @Environment(\.lifecycle) private var lifecycle + @Environment(\.isInDemoMode) private var isInDemoMode + @Environment(\.demoExitPresentation) private var demoExitPresentation + + init( + report: YearReportModel, + recordingWarning: RecordingConfigurationWarningModel? = nil, + navigation: SceneNavigationModel.Settings? = nil, + ) { + self.report = report + self + .recordingWarning = recordingWarning ?? + RecordingConfigurationWarningModel(preferences: report.preferences) + _navigation = State(initialValue: navigation ?? SceneNavigationModel.Settings()) + } + + private var searchQuery: String { + navigation.searchText.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private var isSearching: Bool { + !searchQuery.isEmpty + } + + private var searchResults: [SettingsSearchResult] { + SettingsCatalog.results(matching: searchQuery) + .filter { isAvailable($0.destination) } + } + + /// Whether a group is offered right now. Everything is, except the few + /// groups a demo has no business showing (see + /// `SettingsDestination.isAvailableInDemoMode`) — filtered here rather than + /// per-row so the list and the search index can't disagree about what + /// exists. + private func isAvailable(_ destination: SettingsDestination) -> Bool { + destination.isAvailable(isInDemoMode: isInDemoMode, updatesEnabled: updates != nil) + } + + var body: some View { + @Bindable var navigation = navigation + List(selection: $navigation.selectedRoute) { + if isSearching { + ForEach(searchResults) { result in + searchNavigationRow(result) + } + } else { + if recordingWarning.isPresented { + recordingWarningSection + } + if isInDemoMode, demoExitPresentation == .session { + demoSection + } + ForEach(SettingsListSection.allCases, id: \.self) { section in + let destinations = section.destinations.filter(isAvailable) + if !destinations.isEmpty { + Section { + ForEach(destinations, id: \.self) { destination in + groupNavigationRow(destination) + } + } header: { + if let headerTitle = section.headerTitle { + Text(headerTitle) + } + } + } + } + } + } + .navigationTitle(String(localized: .settingsTitle)) + .searchable( + text: $navigation.searchText, + prompt: String(localized: .settingsSearchPrompt), + ) + .overlay { + if isSearching, searchResults.isEmpty { + ContentUnavailableView.search(text: searchQuery) + } + } + } + + private var recordingWarningSection: some View { + Section { + NavigationLink(value: SettingsRoute(.devices)) { + Label { + VStack(alignment: .leading, spacing: 4) { + Text(.settingsRecordingWarningTitle) + .font(.headline) + Text(.settingsRecordingWarningMessage) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } icon: { + Image(systemSymbol: .exclamationmarkTriangleFill) + .foregroundStyle(.yellow) + .accessibilityHidden(true) + } + } + Button(.settingsRecordingWarningDismiss, role: .cancel) { + recordingWarning.dismiss() + } + } + } + + /// The way out of demo mode, at the very top of the list where a temporary + /// state belongs — above the real settings rather than filed among them. + /// + /// No confirmation: there is nothing to lose. Demo data was never saved, + /// and leaving is exactly what quitting the app would do anyway, so a + /// "are you sure?" would overstate the stakes. + private var demoSection: some View { + Section { + Button { + exitDemoMode() + } label: { + Label( + String(localized: .settingsDemoExit), + systemSymbol: .rectanglePortraitAndArrowRight, + ) + } + } header: { + Text(String(localized: .settingsDemoHeader)) + } footer: { + Text(String(localized: .settingsDemoFooter)) + } + } + + /// Tear the demo world down and re-drive the launch, through the same + /// `LifecycleProxy` the reset flow uses. The relaunch parks on the + /// onboarding gate (or resolves straight through for someone who had + /// already onboarded before trying the demo). + private func exitDemoMode() { + Task { await lifecycle.teardown(WhereLaunch.exitDemoPlan(for: model), input: session) } + } + + /// A top-level row that either pushes its sub-screen or, for a sheet group + /// (Regions), presents it modally. + @ViewBuilder + private func groupNavigationRow(_ destination: SettingsDestination) -> some View { + if destination.isSheet { + Button { present(destination) } label: { groupRow(destination) } + .tint(.primary) + } else { + NavigationLink(value: SettingsRoute(destination)) { groupRow(destination) } + } + } + + /// A search-result row that mirrors the group row's push-vs-sheet routing. + @ViewBuilder + private func searchNavigationRow(_ result: SettingsSearchResult) -> some View { + if result.destination.isSheet { + Button { present(result.destination) } label: { searchRow(result) } + .tint(.primary) + } else { + NavigationLink(value: SettingsRoute(result)) { searchRow(result) } + } + } + + /// Present a sheet group. Non-sheet destinations are pushed via + /// `NavigationLink`, so reaching them here is a programmer error. + private func present(_ destination: SettingsDestination) { + switch destination { + case .regions: + navigation.showRegions = true + case .placesYear, .recording, .privacyBackups, .attachments, .loggedDays, .devices, + .alerts, .appearance, .year, .siri, + .widgets, .shareEvidence, .estimatedTime, .insightsAccuracy, .personalization, + .data, .privacyDiagnostics, .updates, .about: + assertionFailure("\(destination) is a push destination, not a sheet") + } + } + + /// A top-level drill-in row: icon + group title, with a value subtitle where + /// one is cheap and useful (location status, the report year). + private func groupRow(_ destination: SettingsDestination) -> some View { + LabeledContent { + if let subtitle = subtitle(for: destination) { + Text(subtitle) + .foregroundStyle(.secondary) + } + } label: { + Label { + Text(destination.rowTitle) + } icon: { + SettingsIcon(systemSymbol: destination.systemSymbol, color: destination.iconColor) + } + } + } + + /// A cheap, always-available value shown on the right of a group row; `nil` + /// for groups without a meaningful one-line summary. + private func subtitle(for destination: SettingsDestination) -> String? { + switch destination { + case .devices: + LocationStatusRow.statusTitle( + status: session.authorizationStatus, + isTracking: session.isTracking, + ) + case .updates: updates?.attentionTitle + case .year: + report.selectedYear.formatted(.number.grouping(.never)) + case .placesYear, .recording, .privacyBackups, .attachments, .loggedDays, .regions, + .alerts, .appearance, .siri, .widgets, + .shareEvidence, .estimatedTime, .insightsAccuracy, .personalization, .data, + .privacyDiagnostics, .about: + nil + } + } + + /// A search result row: the owning group's icon beside the setting's name + /// over its parent group, so a result reads as belonging to its section. + private func searchRow(_ result: SettingsSearchResult) -> some View { + Label { + VStack(alignment: .leading, spacing: 2) { + Text(result.title) + Text(result.destination.rowTitle) + .font(.caption) + .foregroundStyle(.secondary) + } + } icon: { + SettingsIcon( + systemSymbol: result.destination.systemSymbol, + color: result.destination.iconColor, + ) + } + } +} + +#if DEBUG + #Preview { SettingsView.snapshotPreviews } +#endif diff --git a/Where/WhereUI/Sources/Settings/SettingsView.swift b/Where/WhereUI/Sources/Settings/SettingsView.swift index 6279a4984..20bbf1b72 100644 --- a/Where/WhereUI/Sources/Settings/SettingsView.swift +++ b/Where/WhereUI/Sources/Settings/SettingsView.swift @@ -1,6 +1,7 @@ #if DEBUG @_spi(Testing) import InstallerKitUI #endif +import BroadwayUI import InstallerKit import RegionKit import SFSafeSymbols @@ -15,29 +16,25 @@ import WhereCore /// deep-links to the screen — and the row — containing each. /// /// The top level owns nothing but navigation; behavior lives in the sub-screens -/// (`DevicesSettingsView`, `AlertsSettingsView`, …). The scene's report model and -/// the two view-scoped editing models (backup, reminders) are owned here and -/// handed down; the `WhereSession` coordinator (recording/location) and +/// (`DevicesSettingsView`, `AlertsSettingsView`, …). The scene injects its report. +/// This view owns the backup and reminders models above the adaptive container; +/// the `WhereSession` coordinator (recording/location) and /// `WhereModel` (reset) come from the environment via the sub-screens. struct SettingsView: View { let report: YearReportModel let recordingWarning: RecordingConfigurationWarningModel @State private var backup: BackupModel @State private var reminders: RemindersSettingsModel - @State private var searchText = "" - @State private var showRegions = false + @State private var navigation: SceneNavigationModel.Settings - @Environment(WhereSession.self) private var session - @Environment(UpdateModel.self) private var updates: UpdateModel? - @Environment(WhereModel.self) private var model - @Environment(\.lifecycle) private var lifecycle - @Environment(\.isInDemoMode) private var isInDemoMode - @Environment(\.primaryAppIconName) private var primaryAppIconName + @Environment(\.stylesheet) private var stylesheet init( report: YearReportModel, recordingWarning: RecordingConfigurationWarningModel? = nil, + navigation: SceneNavigationModel.Settings? = nil, ) { + _navigation = State(initialValue: navigation ?? SceneNavigationModel.Settings()) self.report = report self.recordingWarning = recordingWarning ?? RecordingConfigurationWarningModel( preferences: report.preferences, @@ -50,223 +47,78 @@ struct SettingsView: View { )) } - private var searchQuery: String { - searchText.trimmingCharacters(in: .whitespacesAndNewlines) - } - - private var isSearching: Bool { - !searchQuery.isEmpty - } - - private var searchResults: [SettingsSearchResult] { - SettingsCatalog.results(matching: searchQuery) - .filter { isAvailable($0.destination) } - } - - /// Whether a group is offered right now. Everything is, except the few - /// groups a demo has no business showing (see - /// `SettingsDestination.isAvailableInDemoMode`) — filtered here rather than - /// per-row so the list and the search index can't disagree about what - /// exists. - private func isAvailable(_ destination: SettingsDestination) -> Bool { - destination.isAvailable(isInDemoMode: isInDemoMode, updatesEnabled: updates != nil) - } - var body: some View { - NavigationStack { - List { - if isSearching { - ForEach(searchResults) { result in - searchNavigationRow(result) - } - } else { - if recordingWarning.isPresented { - recordingWarningSection - } - if isInDemoMode { - demoSection - } - ForEach(SettingsListSection.allCases, id: \.self) { section in - let destinations = section.destinations.filter(isAvailable) - if !destinations.isEmpty { - Section { - ForEach(destinations, id: \.self) { destination in - groupNavigationRow(destination) - } - } header: { - if let headerTitle = section.headerTitle { - Text(headerTitle) - } - } + @Bindable var navigation = navigation + AdaptiveHistoryContainer { arrangement in + NavigationSplitView( + columnVisibility: .constant(.all), + preferredCompactColumn: $navigation.compactColumn, + ) { + SettingsContent( + report: report, + recordingWarning: recordingWarning, + navigation: navigation, + ) + .navigationSplitViewColumnWidth( + min: stylesheet.adaptiveHistory.minimumControlsWidth, + ideal: stylesheet.adaptiveHistory.minimumHistoryWidth, + ) + } detail: { + NavigationStack(path: $navigation.path) { + Group { + if let route = navigation.selectedRoute { + destination(for: route) + } else { + ContentUnavailableView( + String(localized: .settingsTitle), + systemSymbol: .gearshape, + ) } } - } - } - .navigationTitle(String(localized: .settingsTitle)) - .searchable(text: $searchText, prompt: String(localized: .settingsSearchPrompt)) - .overlay { - if isSearching, searchResults.isEmpty { - ContentUnavailableView.search(text: searchQuery) - } - } - .navigationDestination(for: SettingsRoute.self) { route in - destination(for: route) - } - .sheet(isPresented: $showRegions) { - RegionsSettingsView(usedThisYear: regionsUsedThisYear) - } - } - } - - private var recordingWarningSection: some View { - Section { - NavigationLink(value: SettingsRoute(.devices)) { - Label { - VStack(alignment: .leading, spacing: 4) { - Text(.settingsRecordingWarningTitle) - .font(.headline) - Text(.settingsRecordingWarningMessage) - .font(.subheadline) - .foregroundStyle(.secondary) + .navigationDestination(for: SettingsRoute.self) { route in + destination(for: route) } - } icon: { - Image(systemSymbol: .exclamationmarkTriangleFill) - .foregroundStyle(.yellow) - .accessibilityHidden(true) } - } - Button(.settingsRecordingWarningDismiss, role: .cancel) { - recordingWarning.dismiss() - } - } - } - - /// The way out of demo mode, at the very top of the list where a temporary - /// state belongs — above the real settings rather than filed among them. - /// - /// No confirmation: there is nothing to lose. Demo data was never saved, - /// and leaving is exactly what quitting the app would do anyway, so a - /// "are you sure?" would overstate the stakes. - private var demoSection: some View { - Section { - Button { - exitDemoMode() - } label: { - Label( - String(localized: .settingsDemoExit), - systemSymbol: .rectanglePortraitAndArrowRight, + .navigationSplitViewColumnWidth( + min: stylesheet.adaptiveHistory.minimumHistoryWidth, + ideal: stylesheet.adaptiveHistory.minimumHistoryWidth, ) } - } header: { - Text(String(localized: .settingsDemoHeader)) - } footer: { - Text(String(localized: .settingsDemoFooter)) + .navigationSplitViewStyle(.balanced) + .environment(\.horizontalSizeClass, arrangement == .compact ? .compact : .regular) + .bHorizontalSizeClass(arrangement == .compact ? .compact : .regular) } - } - - /// Tear the demo world down and re-drive the launch, through the same - /// `LifecycleProxy` the reset flow uses. The relaunch parks on the - /// onboarding gate (or resolves straight through for someone who had - /// already onboarded before trying the demo). - private func exitDemoMode() { - Task { await lifecycle.teardown(WhereLaunch.exitDemoPlan(for: model), input: session) } - } - - /// A top-level row that either pushes its sub-screen or, for a sheet group - /// (Regions), presents it modally. - @ViewBuilder - private func groupNavigationRow(_ destination: SettingsDestination) -> some View { - if destination.isSheet { - Button { present(destination) } label: { groupRow(destination) } - .tint(.primary) - } else { - NavigationLink(value: SettingsRoute(destination)) { groupRow(destination) } - } - } - - /// A search-result row that mirrors the group row's push-vs-sheet routing. - @ViewBuilder - private func searchNavigationRow(_ result: SettingsSearchResult) -> some View { - if result.destination.isSheet { - Button { present(result.destination) } label: { searchRow(result) } - .tint(.primary) - } else { - NavigationLink(value: SettingsRoute(result)) { searchRow(result) } + .sheet(isPresented: $navigation.showRegions) { + RegionsSettingsView(usedThisYear: regionsUsedThisYear) } } - /// Present a sheet group. Non-sheet destinations are pushed via - /// `NavigationLink`, so reaching them here is a programmer error. - private func present(_ destination: SettingsDestination) { - switch destination { - case .regions: - showRegions = true - case .placesYear, .recording, .privacyBackups, .attachments, .loggedDays, .devices, - .alerts, .appearance, .year, .siri, - .widgets, .shareEvidence, .estimatedTime, .insightsAccuracy, .personalization, - .data, .privacyDiagnostics, .updates, .about: - assertionFailure("\(destination) is a push destination, not a sheet") - } - } - - /// A top-level drill-in row: icon + group title, with a value subtitle where - /// one is cheap and useful (location status, the report year). - private func groupRow(_ destination: SettingsDestination) -> some View { - LabeledContent { - if let subtitle = subtitle(for: destination) { - Text(subtitle) - .foregroundStyle(.secondary) - } - } label: { - Label { - Text(destination.rowTitle) - } icon: { - SettingsIcon(systemSymbol: destination.systemSymbol, color: destination.iconColor) - } - } + private func destination(for route: SettingsRoute) -> some View { + SettingsDestinationView(route: route, report: report, backup: backup, reminders: reminders) } - /// A cheap, always-available value shown on the right of a group row; `nil` - /// for groups without a meaningful one-line summary. - private func subtitle(for destination: SettingsDestination) -> String? { - switch destination { - case .devices: - LocationStatusRow.statusTitle( - status: session.authorizationStatus, - isTracking: session.isTracking, - ) - case .updates: - updates?.attentionTitle - case .year: - report.selectedYear.formatted(.number.grouping(.never)) - case .placesYear, .recording, .privacyBackups, .attachments, .loggedDays, .regions, - .alerts, .appearance, .siri, .widgets, - .shareEvidence, .estimatedTime, .insightsAccuracy, .personalization, .data, - .privacyDiagnostics, .about: - nil - } + /// Regions with days in the selected report year, so the region editor can + /// surface a "used this year" group (grouping order only — it doesn't affect + /// what's saved). `.other` isn't a pickable region, so it's dropped. + private var regionsUsedThisYear: Set { + guard let totals = report.report?.totals else { return [] } + return Set(totals.filter { $0.key != .other && $0.value > 0 }.map(\.key)) } +} - /// A search result row: the owning group's icon beside the setting's name - /// over its parent group, so a result reads as belonging to its section. - private func searchRow(_ result: SettingsSearchResult) -> some View { - Label { - VStack(alignment: .leading, spacing: 2) { - Text(result.title) - Text(result.destination.rowTitle) - .font(.caption) - .foregroundStyle(.secondary) - } - } icon: { - SettingsIcon( - systemSymbol: result.destination.systemSymbol, - color: result.destination.iconColor, - ) - } - } +/// The shared typed destination builder for both adaptive navigation and standalone panes. +private struct SettingsDestinationView: View { + let route: SettingsRoute + let report: YearReportModel + let backup: BackupModel + let reminders: RemindersSettingsModel + @Environment(UpdateModel.self) private var updates: UpdateModel? + @Environment(\.isInDemoMode) private var isInDemoMode + @Environment(WhereSession.self) private var session + @Environment(WhereModel.self) private var model + @Environment(\.primaryAppIconName) private var primaryAppIconName - @ViewBuilder - private func destination(for route: SettingsRoute) -> some View { + var body: some View { switch route.destination { case .attachments: EvidenceListView(report: report) @@ -343,14 +195,6 @@ struct SettingsView: View { } } - /// Regions with days in the selected report year, so the region editor can - /// surface a "used this year" group (grouping order only — it doesn't affect - /// what's saved). `.other` isn't a pickable region, so it's dropped. - private var regionsUsedThisYear: Set { - guard let totals = report.report?.totals else { return [] } - return Set(totals.filter { $0.key != .other && $0.value > 0 }.map(\.key)) - } - private var featureDiscoveryPresentation: FeatureDiscoveryPresentation { FeatureDiscoveryPresentation( report: report.report, @@ -366,6 +210,15 @@ struct SettingsView: View { /// The extra right-to-left variant exercises the RTL configuration axis /// on a directional screen (leading labels, trailing values/toggles). static var snapshots: [SnapshotCase] { + whereSnapshot( + name: "BrowseColumn", + configurations: [SnapshotConfiguration(device: AdaptiveHistorySnapshotFixture + .browseColumn)], + ) { + snapshotList(report: PreviewSupport.loadedYearReportModel()) + .environment(PreviewSupport.loadedModel()) + .environment(PreviewSupport.loadedSession()) + } whereSnapshot( name: "Default", configurations: .fullContentScreenRepresentative + [ @@ -375,7 +228,7 @@ struct SettingsView: View { ), ], ) { - SettingsView(report: PreviewSupport.loadedYearReportModel()) + snapshotList(report: PreviewSupport.loadedYearReportModel()) .environment(PreviewSupport.loadedModel()) .environment(PreviewSupport.loadedSession()) } @@ -403,7 +256,7 @@ struct SettingsView: View { name: "DemoMode", configurations: .fullContentPhoneLightDark, ) { - SettingsView(report: PreviewSupport.loadedYearReportModel()) + snapshotList(report: PreviewSupport.loadedYearReportModel()) .environment(PreviewSupport.loadedModel()) .environment(PreviewSupport.loadedSession()) .environment(\.isInDemoMode, true) @@ -412,7 +265,7 @@ struct SettingsView: View { name: "RecordingConfigurationWarning", configurations: .fullContentPhoneLightDark, ) { - SettingsView( + snapshotList( report: PreviewSupport.loadedYearReportModel(), recordingWarning: PreviewSupport.recordingConfigurationWarningModel(), ) @@ -420,6 +273,29 @@ struct SettingsView: View { .environment(PreviewSupport.loadedSession()) } } + + private static func snapshotList( + report: YearReportModel, + recordingWarning: RecordingConfigurationWarningModel? = nil, + ) -> some View { + let backup = BackupModel(services: report.services) + let reminders = RemindersSettingsModel( + services: report.services, + preferences: report.preferences, + now: report.now, + ) + return NavigationStack { + SettingsContent(report: report, recordingWarning: recordingWarning) + .navigationDestination(for: SettingsRoute.self) { route in + SettingsDestinationView( + route: route, + report: report, + backup: backup, + reminders: reminders, + ) + } + } + } } #Preview { diff --git a/Where/WhereUI/Sources/Year/YearView.swift b/Where/WhereUI/Sources/Year/YearView.swift index edb64f0f2..37b82c0d0 100644 --- a/Where/WhereUI/Sources/Year/YearView.swift +++ b/Where/WhereUI/Sources/Year/YearView.swift @@ -9,28 +9,109 @@ import WhereCore struct YearView: View { let report: YearReportModel - @State private var mode: YearMode + @State private var navigation: SceneNavigationModel.Year @Environment(\.stylesheet) private var stylesheet + init(report: YearReportModel, navigation: SceneNavigationModel.Year) { + self.report = report + _navigation = State(initialValue: navigation) + } + init(report: YearReportModel, initialMode: YearMode = .calendar) { + self.init(report: report, navigation: SceneNavigationModel.Year(mode: initialMode)) + } + + var body: some View { + AdaptiveHistoryContainer { arrangement in + switch arrangement { + case .compact: + YearCompactView(report: report, navigation: navigation) + case .columns: + NavigationSplitView(columnVisibility: .constant(.all)) { + timeline(presentation: .linked) + .navigationSplitViewColumnWidth( + min: stylesheet.adaptiveHistory.minimumControlsWidth, + ideal: stylesheet.adaptiveHistory.minimumControlsWidth, + ) + } detail: { + calendar(presentation: .year) + .navigationSplitViewColumnWidth( + min: stylesheet.adaptiveHistory.minimumHistoryWidth, + ideal: stylesheet.adaptiveHistory.minimumHistoryWidth, + ) + } + .navigationSplitViewStyle(.balanced) + case let .tabletop(panes): + TabletopHistoryLayout(panes: panes) { + NavigationStack { calendar(presentation: .month) } + } controls: { + NavigationStack { + timeline(presentation: .linked) + .safeAreaInset(edge: .top, spacing: 0) { + HistoryMonthControls( + report: report, + history: navigation.history, + ) + } + } + } + } + } + .modifier(PlanningPresentation(report: report, navigation: navigation.history.planning)) + } + + private func calendar(presentation: CalendarContentView.Presentation) -> some View { + CalendarContentView( + report: report, + history: navigation.history, + presentation: presentation, + persistenceKey: .year, + ) + .navigationTitle(String(localized: .primaryCalendar)) + .navigationBarTitleDisplayMode(.inline) + } + + private func timeline(presentation: PresenceTimelineList.Presentation) -> some View { + PresenceTimelineList( + report: report, + presentation: presentation, + history: navigation.history, + ) + .navigationTitle(String(localized: .primaryTimeline)) + .navigationBarTitleDisplayMode(.inline) + } +} + +/// The single-pane production presentation, also captured in full for content completeness. +struct YearCompactView: View { + let report: YearReportModel + @State private var navigation: SceneNavigationModel.Year + @Environment(\.stylesheet) private var stylesheet + + init(report: YearReportModel, navigation: SceneNavigationModel.Year? = nil) { self.report = report - _mode = State(initialValue: initialMode) + _navigation = State(initialValue: navigation ?? SceneNavigationModel.Year()) } var body: some View { + @Bindable var navigation = navigation NavigationStack { Group { - switch mode { + switch navigation.mode { case .calendar: - CalendarContentView(report: report, persistenceKey: .year) + CalendarContentView( + report: report, + history: navigation.history, + persistenceKey: .year, + ) case .timeline: - PresenceTimelineList(report: report) + PresenceTimelineList(report: report, history: navigation.history) } } // Crossfade between the two views rather than hard-cutting. - .animation(.default, value: mode) - .navigationTitle(mode.title) + .animation(.default, value: navigation.mode) + .navigationTitle(navigation.mode.title) .navigationBarTitleDisplayMode(.inline) // Keep the bar background on at all times. The calendar auto-scrolls // under the bar (so its scroll-edge material is showing) while the @@ -38,7 +119,7 @@ struct YearView: View { // them animates that material in/out — reading as a toolbar fade. .toolbarBackground(.visible, for: .navigationBar) .safeAreaInset(edge: .bottom, alignment: .center) { - YearModePicker(mode: $mode) + YearModePicker(mode: $navigation.mode) .padding(.bottom, stylesheet.spacing.xLarge) } } @@ -135,14 +216,14 @@ private struct YearModePicker: View { measurementReadiness: .immediate, settle: .settledAtLeast(minDuration: 1.0), ) { - YearView(report: PreviewSupport.loadedYearReportModel()) + YearCompactView(report: PreviewSupport.loadedYearReportModel()) } whereSnapshot( name: "Empty", configurations: .fullContentPhoneLightDark, measurementReadiness: .immediate, ) { - YearView(report: PreviewSupport.emptyYearReportModel()) + YearCompactView(report: PreviewSupport.emptyYearReportModel()) } whereSnapshot( name: "RestoredOverview", diff --git a/Where/WhereUI/Tests/CalendarContentViewTests.swift b/Where/WhereUI/Tests/CalendarContentViewTests.swift index 2cf876437..bcc844fce 100644 --- a/Where/WhereUI/Tests/CalendarContentViewTests.swift +++ b/Where/WhereUI/Tests/CalendarContentViewTests.swift @@ -120,3 +120,111 @@ struct CalendarContentViewTests { return visit(root) } } + +@MainActor +struct CalendarContentViewTestsDuo { + @Test func repeatedLinkedSelectionExpandsOverviewAndKeepsMonthTarget() async throws { + let report = PreviewSupport.loadedYearReportModel() + let history = HistoryNavigationModel() + let presentation = history.calendar.presentation( + report: report, + initialMode: .overview, + persistenceKey: .year, + ) + let month = CalendarDay(year: report.selectedYear, month: 2, day: 1) + let host = UIHostingController(rootView: CalendarContentView( + report: report, + history: history, + persistenceKey: .year, + ).whereBroadwayRoot()) + try await show(host) { _ in + try await waitForAsync { presentation.year == report.selectedYear } + for _ in 0 ..< 2 { + history.select(month: month, dateCalendar: report.calendar) + try await waitForAsync { + history.calendar.pendingReveal == nil && presentation.mode == .detail + && history.calendar.calendarScrollPosition + .viewID(type: CalendarPresentationModel.Target.self) == .month(month) + } + presentation.toggle() + #expect(presentation.mode == .overview) + } + } + } + + @Test func regionZoomPreferencesRemainIndependentInRetainedNavigation() { + let report = PreviewSupport.loadedYearReportModel() + let navigation = CalendarNavigationModel() + let california = navigation.presentation( + report: report, + initialMode: .detail, + persistenceKey: .region(.california), + ) + let newYork = navigation.presentation( + report: report, + initialMode: .detail, + persistenceKey: .region(.newYork), + ) + california.toggle() + #expect(california.mode == .overview) + #expect(newYork.mode == .detail) + #expect(navigation.presentation( + report: report, + initialMode: .detail, + persistenceKey: .region(.california), + ) === california) + } + + @Test func mountedCalendarPositionsAfterTheNewYearsMonthsArrive() async throws { + let report = PreviewSupport.loadedYearReportModel() + let navigation = CalendarNavigationModel() + let host = UIHostingController(rootView: CalendarContentView( + report: report, + navigation: navigation, + ) + .whereBroadwayRoot()) + try await show(host) { _ in + try await waitForAsync { navigation.initiallyPositionedYear == report.selectedYear } + let previousYear = report.selectedYear - 1 + await report.select(year: previousYear) + try await waitForAsync { navigation.initiallyPositionedYear == previousYear } + #expect(navigation.scrollPosition.viewID(type: CalendarDay.self) == CalendarDay( + year: previousYear, + month: 12, + day: 1, + )) + } + } + + @Test func aRemountedCalendarUsesTheSceneAnchor() async throws { + let report = PreviewSupport.loadedYearReportModel() + let scene = SceneNavigationModel() + let navigation = scene.year.calendar + let browsed = CalendarDay(year: report.selectedYear, month: 2, day: 1) + let first = UIHostingController(rootView: CalendarContentView( + report: report, + navigation: navigation, + ) + .whereBroadwayRoot()) + try await show(first) { _ in + try await waitForAsync { + navigation.initiallyPositionedYear == report.selectedYear + } + } + navigation.recordVisibleMonths([browsed]) + navigation.presentation(report: report, initialMode: .detail, persistenceKey: nil) + .remember(.month(browsed)) + let second = UIHostingController(rootView: CalendarContentView( + report: report, + navigation: navigation, + ) + .whereBroadwayRoot()) + try await show(second) { _ in + try await waitForAsync { + navigation.calendarScrollPosition + .viewID(type: CalendarPresentationModel.Target.self) == .month(browsed) + } + #expect(scene.locations.calendar.initiallyPositionedYear == nil) + } + } +} diff --git a/Where/WhereUI/Tests/DeveloperDestinationTests.swift b/Where/WhereUI/Tests/DeveloperDestinationTests.swift index cdb1721ea..5a99c4d82 100644 --- a/Where/WhereUI/Tests/DeveloperDestinationTests.swift +++ b/Where/WhereUI/Tests/DeveloperDestinationTests.swift @@ -10,6 +10,7 @@ .tool(.logs), .tool(.openSpans), .flyover, + .tool(.adaptiveHistory), .tool(.regionMap), .tool(.crashTesting), ], diff --git a/Where/WhereUI/Tests/DeveloperTabBarInsetTests.swift b/Where/WhereUI/Tests/DeveloperTabBarInsetTests.swift index fa4cd3654..1c274a86e 100644 --- a/Where/WhereUI/Tests/DeveloperTabBarInsetTests.swift +++ b/Where/WhereUI/Tests/DeveloperTabBarInsetTests.swift @@ -1,18 +1,42 @@ import SwiftUI import TestHostSupport import Testing +import UIKit @testable import WhereUI @MainActor struct DeveloperTabBarInsetTests { - @Test func reduceKeepsTheLargestReportedInset() { + @Test func reduceKeepsEachEdgesLargestReportedInset() { var value = DeveloperTabBarInsetKey.defaultValue - DeveloperTabBarInsetKey.reduce(value: &value) { 40 } - #expect(value == 40) - DeveloperTabBarInsetKey.reduce(value: &value) { 10 } - #expect(value == 40) - DeveloperTabBarInsetKey.reduce(value: &value) { 55 } - #expect(value == 55) + DeveloperTabBarInsetKey.reduce(value: &value) { EdgeInsets( + top: 2, + leading: 4, + bottom: 40, + trailing: 8, + ) } + DeveloperTabBarInsetKey.reduce(value: &value) { EdgeInsets( + top: 1, + leading: 12, + bottom: 10, + trailing: 6, + ) } + #expect(value == EdgeInsets(top: 2, leading: 12, bottom: 40, trailing: 8)) + } + + @Test func sideBarsUseTheAttachedWindowsPhysicalInsets() { + let content = EdgeInsets(top: 8, leading: 64, bottom: 34, trailing: 84) + let window = UIEdgeInsets(top: 0, left: 0, bottom: 34, right: 84) + #expect(DeveloperBarInsets.additional( + content: content, + window: window, + direction: .leftToRight, + ) + == EdgeInsets(top: 8, leading: 64, bottom: 0, trailing: 0)) + #expect(DeveloperBarInsets.additional( + content: EdgeInsets(top: 8, leading: 84, bottom: 34, trailing: 64), + window: window, + direction: .rightToLeft, + ) == EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 64)) } /// The reporter must measure the *floating tab bar*, not just the home @@ -33,8 +57,8 @@ struct DeveloperTabBarInsetTests { .onPreferenceChange(DeveloperTabBarInsetKey.self) { box.value = $0 } try show(UIHostingController(rootView: root)) { _ in - try waitFor { box.value > 0 } - #expect(box.value > 0) + try waitFor { box.value.bottom > 0 } + #expect(box.value.bottom > 0) } } } @@ -42,5 +66,5 @@ struct DeveloperTabBarInsetTests { /// Bridges the `@Sendable` `onPreferenceChange` callback back to the test. Reads /// and writes are confined to the main actor by the hosted run loop. private final class InsetBox: @unchecked Sendable { - var value: CGFloat = 0 + var value = EdgeInsets() } diff --git a/Where/WhereUI/Tests/LocationForecastModelTests.swift b/Where/WhereUI/Tests/LocationForecastModelTests.swift index 75a051fbe..705fd4089 100644 --- a/Where/WhereUI/Tests/LocationForecastModelTests.swift +++ b/Where/WhereUI/Tests/LocationForecastModelTests.swift @@ -106,8 +106,8 @@ struct LocationForecastModelTests { let firstStay = try PlanningModelTestSupport.stay(region: .newYork) let secondStay = try PlanningModelTestSupport.stay(region: .california) try await model.create(stay: firstStay) - let firstGate = TestStore.PlanningReadGate() - let secondGate = TestStore.PlanningReadGate() + let firstGate = TestStore.PlanningOperationGate() + let secondGate = TestStore.PlanningOperationGate() await store.gateNextPlanningRead(with: firstGate) let firstRefresh = Task { await model.refresh() } @@ -150,8 +150,8 @@ struct LocationForecastModelTests { await model.refresh() let lastGoodSnapshot = model.planning try await model.create(stay: secondStay) - let firstGate = TestStore.PlanningReadGate() - let secondGate = TestStore.PlanningReadGate() + let firstGate = TestStore.PlanningOperationGate() + let secondGate = TestStore.PlanningOperationGate() await store.gateNextPlanningRead(with: firstGate) let firstRefresh = Task { await model.refresh() } @@ -184,7 +184,7 @@ struct LocationForecastModelTests { let stay = try PlanningModelTestSupport.stay(region: .newYork) try await model.create(stay: stay) await store.failPlanningReads() - let gate = TestStore.PlanningReadGate() + let gate = TestStore.PlanningOperationGate() await store.gateNextPlanningRead(with: gate) let failingRefresh = Task { await model.refresh() } await gate.waitUntilReached() @@ -208,7 +208,7 @@ struct LocationForecastModelTests { let model = PlanningModelTestSupport.model(store: store) let stay = try PlanningModelTestSupport.stay(region: .newYork) try await model.create(stay: stay) - let gate = TestStore.PlanningReadGate() + let gate = TestStore.PlanningOperationGate() await store.gateNextPlanningRead(with: gate) let activeRefresh = Task { await model.refresh() } await gate.waitUntilReached() diff --git a/Where/WhereUI/Tests/LocationsViewTests.swift b/Where/WhereUI/Tests/LocationsViewTests.swift index 496d856a7..194147dc3 100644 --- a/Where/WhereUI/Tests/LocationsViewTests.swift +++ b/Where/WhereUI/Tests/LocationsViewTests.swift @@ -1,5 +1,7 @@ import SwiftUI +import TestHostSupport import Testing +import WhereCore @testable import WhereUI struct LocationsViewTests { @@ -7,3 +9,109 @@ struct LocationsViewTests { #expect(MemoryLayout.size < 16384) } } + +/// Both tests resize native navigation in the shared test host window. +@MainActor +@Suite(.serialized) +struct LocationsViewTestsDuo { + @Test func initialBrowsePositionFollowsInsetsUntilARegionIsRestored() async throws { + let world = try await WhereFlyoverWorld.build() + let navigation = SceneNavigationModel.Locations() + let host = UIHostingController(rootView: LocationsBrowsePane( + report: world.report, + navigation: navigation, + ) + .dynamicTypeSize(.accessibility5) + .whereBroadwayRoot()) + + try await show(host) { controller in + controller.view.frame = CGRect(x: 0, y: 0, width: 466, height: 678) + controller.traitOverrides.horizontalSizeClass = .compact + controller.view.layoutIfNeeded() + try await waitForAsync { + AdaptiveHistoryTestSupport.descendants( + of: controller.view, + matching: UIScrollView.self, + ).contains { $0.contentSize.height > $0.bounds.height } + } + let scroll = try #require(AdaptiveHistoryTestSupport.descendants( + of: controller.view, + matching: UIScrollView.self, + ).first { $0.contentSize.height > $0.bounds.height }) + for topInset in [CGFloat(82), 0, 56, 0] { + let previousInset = scroll.adjustedContentInset.top + controller.additionalSafeAreaInsets.top = topInset + controller.view.layoutIfNeeded() + try await waitForAsync { + abs(scroll.adjustedContentInset.top - previousInset) > 1 + && abs(scroll.contentOffset.y + scroll.adjustedContentInset.top) < 1 + } + } + navigation.visibleRegion = .california + navigation.restoreBrowsePosition() + try await waitForAsync { + scroll.contentOffset.y + scroll.adjustedContentInset.top > 100 + } + #expect(navigation.selectedRegion == nil) + } + } + + @Test func foldingDuringLoadingAndAYearChangeDoesNotRestartOrPublishTheOlderRead() async throws { + let store = try TestStore() + let report = PlanningTestSupport.report(store: store) + let calendar = report.calendar + try await report.services.journal.addManualDay( + date: CalendarDay(year: 2024, month: 3, day: 1).startOfDay(in: calendar), + regions: [.newYork], + audit: nil, + ) + try await report.services.journal.addManualDay( + date: CalendarDay(year: 2026, month: 3, day: 1).startOfDay(in: calendar), + regions: [.california], + audit: nil, + ) + await store.enableFirstSamplesGate() + let olderRead = Task { await report.select(year: 2024) } + await store.awaitFirstSamplesCall() + let navigation = SceneNavigationModel.Locations() + navigation.select(region: .california) + let state = AdaptiveHistoryHostState() + let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + LocationsView(report: report, navigation: navigation) + }) + host.safeAreaRegions = [] + do { + try await show(host) { controller in + let readsBeforeFolding = await store.sampleReadCount + for pose in [ + AdaptiveHistoryPose.outerPortrait, + .innerPortrait, + .tabletop, + .outerPortrait, + ] { + try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) + #expect(await store.sampleReadCount == readsBeforeFolding) + #expect(report.loadState == .loading) + } + await report.select(year: 2026) + let readsAfterYearChange = await store.sampleReadCount + for pose in [AdaptiveHistoryPose.innerLandscape, .tabletop, .outerPortrait] { + try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) + #expect(await store.sampleReadCount == readsAfterYearChange) + } + await store.releaseFirstSamplesCall() + await olderRead.value + #expect(report.selectedYear == 2026) + #expect(report.report?.year == 2026) + #expect(report.report?.totals[.california] == 1) + #expect(report.report?.totals[.newYork] == nil) + #expect(report.loadState == .loaded) + #expect(navigation.selectedRegion == .california) + } + } catch { + await store.releaseFirstSamplesCall() + await olderRead.value + throw error + } + } +} diff --git a/Where/WhereUI/Tests/MainTabsTests.swift b/Where/WhereUI/Tests/MainTabsTests.swift index eb839098e..c6c0dbbfc 100644 --- a/Where/WhereUI/Tests/MainTabsTests.swift +++ b/Where/WhereUI/Tests/MainTabsTests.swift @@ -72,3 +72,70 @@ struct MainTabsTests { } } } + +@MainActor +struct MainTabsTestsDuo { + @Test func foldingKeepsOnePresentedEditorAndItsUnsavedDate() async throws { + let world = try await WhereFlyoverWorld.build() + let navigation = SceneNavigationModel() + let state = AdaptiveHistoryHostState() + let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + MainTabs(world: world, navigation: navigation) + .environment(world.model) + .environment(world.session) + .environment(\.isInDemoMode, true) + }) + host.safeAreaRegions = [] + try await show(host) { controller in + try await AdaptiveHistoryTestSupport.resize( + controller, + state: state, + to: .outerPortrait, + ) + navigation.locations.select(region: .california) + let month = CalendarDay(year: world.report.selectedYear, month: 2, day: 1) + navigation.locations.calendar.reveal(month: month) + navigation.settings.searchText = "appearance" + navigation.year.mode = .timeline + navigation.locations.planningDestination = .new(.california) + let root = try #require(controller.view.window?.rootViewController) + try await waitForAsync { + AdaptiveHistoryTestSupport.presentedController(from: root) != nil + } + let editor = try #require(AdaptiveHistoryTestSupport.presentedController(from: root)) + defer { editor.dismiss(animated: false) } + try await waitForAsync { + !AdaptiveHistoryTestSupport + .descendants(of: editor.view, matching: UIDatePicker.self).isEmpty + } + let picker = try #require(AdaptiveHistoryTestSupport.descendants( + of: editor.view, + matching: UIDatePicker.self, + ).first) + let draft = CalendarDay(year: world.report.selectedYear, month: 9, day: 8) + .startOfDay(in: world.report.calendar) + picker.date = draft + picker.sendActions(for: .valueChanged) + for pose in [ + AdaptiveHistoryPose.innerPortrait, + .tabletop, + .outerPortrait, + .innerLandscape, + .outerPortrait, + ] { + try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) + #expect(AdaptiveHistoryTestSupport.presentedController(from: root) === editor) + #expect(navigation.locations.selectedRegion == .california) + #expect(navigation.locations.calendar.displayedMonth == month) + #expect(navigation.settings.searchText == "appearance") + #expect(navigation.year.mode == .timeline) + #expect(AdaptiveHistoryTestSupport.descendants( + of: editor.view, + matching: UIDatePicker.self, + ).contains { + world.report.calendar.isDate($0.date, inSameDayAs: draft) + }) + } + } + } +} diff --git a/Where/WhereUI/Tests/PlannedStayEditorTests.swift b/Where/WhereUI/Tests/PlannedStayEditorTests.swift new file mode 100644 index 000000000..2ebeeb96b --- /dev/null +++ b/Where/WhereUI/Tests/PlannedStayEditorTests.swift @@ -0,0 +1,59 @@ +import SwiftUI +import TestHostSupport +import Testing +import WhereCore +@_spi(Testing) @testable import WhereUI + +@MainActor +struct PlannedStayEditorTests { + @Test(arguments: [false, true]) + func resizingDuringSavePreservesOneWriteAndItsResult(fails: Bool) async throws { + let store = try TestStore() + let report = PlanningTestSupport.report(store: store) + await report.refresh() + let model = PlannedStayEditorModel(report: report, mode: .new(initialRegion: .california)) + let state = AdaptiveHistoryHostState() + let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + PlannedStayEditor(model: model) + }) + host.safeAreaRegions = [] + let gate = TestStore.PlanningOperationGate() + await store.gateNextPlannedStayWrite(with: gate) + if fails { await store.failPlannedStays() } + try await show(host) { controller in + let saving = Task { await model.save() } + await gate.waitUntilReached() + do { + for pose in [ + AdaptiveHistoryPose.outerPortrait, + .innerPortrait, + .tabletop, + .outerPortrait, + ] { + try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) + #expect(model.isSaving) + #expect(model.region == .california) + #expect(await store.plannedStayWriteCount == 1) + } + #expect(await model.save() == false) + #expect(await store.plannedStayWriteCount == 1) + await gate.release() + #expect(await saving.value == !fails) + #expect(!model.isSaving) + if fails { + guard case let .failed(error) = model.saveState else { + Issue.record("The failed save must remain visible after resizing") + return + } + #expect(error is PlannedStaySaveFailure) + } + let snapshot = try await report.services.plannedStays.snapshot() + #expect(snapshot.stays.count == (fails ? 0 : 1)) + } catch { + await gate.release() + _ = await saving.value + throw error + } + } + } +} diff --git a/Where/WhereUI/Tests/PresenceTimelineListTests.swift b/Where/WhereUI/Tests/PresenceTimelineListTests.swift new file mode 100644 index 000000000..25238a53d --- /dev/null +++ b/Where/WhereUI/Tests/PresenceTimelineListTests.swift @@ -0,0 +1,50 @@ +import SwiftUI +import TestHostSupport +import Testing +import WhereCore +@testable import WhereUI + +@MainActor +struct PresenceTimelineListTests { + @Test func selectingTheSameMonthAgainRevealsItAfterIndependentBrowsing() async throws { + let world = try await WhereFlyoverWorld.build() + let report = try #require(world.report.report) + let history = HistoryNavigationModel() + let month = CalendarDay(year: report.year, month: 5, day: 1) + let end = CalendarDay(year: report.year, month: 5, day: 31) + let stints = PresenceTimeline.stints(from: report, calendar: world.report.calendar) + let items = PresenceJourneyItem.items( + stints: stints, + flights: world.report.completedFlights, + year: report.year, + calendar: world.report.calendar, + ) + let displayedStints = items.compactMap { item -> RegionStint? in + if case let .stay(stint) = item { return stint } + return nil + } + let expected = try #require(displayedStints.first { + CalendarDay(from: $0.start, in: world.report.calendar) <= end + && CalendarDay(from: $0.end, in: world.report.calendar) >= month + }) + let browsed = try #require(displayedStints.last) + #expect(expected.id != browsed.id) + let host = UIHostingController(rootView: PresenceTimelineList( + report: world.report, + presentation: .linked, + history: history, + ).whereBroadwayRoot()) + + try await show(host) { _ in + for _ in 0 ..< 4 { + history.select(month: month, dateCalendar: world.report.calendar) + try await waitForAsync { history.timeline.visibleItem == .recorded(expected.id) } + #expect(history.timeline.requestedMonth == nil) + let selectedInterval = history.selection + history.timeline.recordVisibleItems([.recorded(browsed.id)], in: report.year) + #expect(history.timeline.visibleItem == .recorded(browsed.id)) + #expect(history.selection == selectedInterval) + } + } + } +} diff --git a/Where/WhereUI/Tests/Support/AdaptiveHistoryLoadFixture.swift b/Where/WhereUI/Tests/Support/AdaptiveHistoryLoadFixture.swift new file mode 100644 index 000000000..e1d1caa92 --- /dev/null +++ b/Where/WhereUI/Tests/Support/AdaptiveHistoryLoadFixture.swift @@ -0,0 +1,50 @@ +import Foundation +import SnapshotKit +@_spi(Testing) import WhereCore +@testable import WhereUI + +/// Drives the real report through controlled store reads. No presentation state is injected. +@MainActor +final class AdaptiveHistoryLoadFixture { + enum State: String, CaseIterable { + case loading + case empty + case failed + } + + let report: YearReportModel + private let store: TestStore + private let refresh: Task + + init(state: State) async throws { + let store = try TestStore() + self.store = store + switch state { + case .loading: await store.enableFirstSamplesGate() + case .empty: break + case .failed: await store.failSamples() + } + let now = PreviewSupport.referenceNow + let report = YearReportModel( + services: WhereServices( + store: store, + locationSource: ScriptedLocationSource(), + now: { now }, + ), + selectedYear: PreviewSupport.year, + preferences: PreviewSupport.previewPreferences(), + now: { now }, + ) + self.report = report + refresh = Task { await report.refresh() } + switch state { + case .loading: await store.awaitFirstSamplesCall() + case .empty, .failed: await refresh.value + } + } + + func finish() async { + await store.releaseFirstSamplesCall() + await refresh.value + } +} diff --git a/Where/WhereUI/Tests/Support/TestStore.swift b/Where/WhereUI/Tests/Support/TestStore.swift index 9bd499c05..b8b2a4f0e 100644 --- a/Where/WhereUI/Tests/Support/TestStore.swift +++ b/Where/WhereUI/Tests/Support/TestStore.swift @@ -9,7 +9,11 @@ struct PlannedStaySaveFailure: Error, Equatable {} /// Thrown by `TestStore.samples(in:)` when failure injection is enabled, so a /// year-report load can be forced to fail. -struct SampleReadFailure: Error, Equatable {} +struct SampleReadFailure: LocalizedError, Equatable { + var errorDescription: String? { + "The history store is unavailable." + } +} /// Thrown by the Devices settings save-failure hooks below. struct RecordingDeviceSaveFailure: Error, Equatable {} @@ -29,8 +33,8 @@ struct RecordingDeviceSaveFailure: Error, Equatable {} /// /// Everything else forwards to the backing store so reads stay deterministic. actor TestStore: WhereStore { - /// Holds one captured planning result until the test chooses its completion order. - actor PlanningReadGate { + /// Holds one planning read or write at its suspension boundary until the test releases it. + actor PlanningOperationGate { private enum State { case waiting case suspended(CheckedContinuation) @@ -47,7 +51,7 @@ actor TestStore: WhereStore { } func suspend() async { - precondition(!hasArrived, "A planning read gate must only suspend one read") + precondition(!hasArrived, "A planning operation gate must only suspend one operation") hasArrived = true arrival?.resume() arrival = nil @@ -65,6 +69,7 @@ actor TestStore: WhereStore { private var gateFirstSamplesCall = false private var firstSamplesSeen = false + private(set) var sampleReadCount = 0 private var gate: CheckedContinuation? private var arrival: CheckedContinuation? @@ -73,7 +78,9 @@ actor TestStore: WhereStore { private var recordingDevicesGate: CheckedContinuation? private var recordingDevicesArrival: CheckedContinuation? - private var nextPlanningReadGate: PlanningReadGate? + private var nextPlanningOperationGate: PlanningOperationGate? + private var nextPlannedStayWriteGate: PlanningOperationGate? + private(set) var plannedStayWriteCount = 0 private var shouldFailManualDay = false private var shouldFailPlannedStay = false @@ -135,15 +142,20 @@ actor TestStore: WhereStore { shouldFailPlanningRead = false } - func gateNextPlanningRead(with gate: PlanningReadGate) { - precondition(nextPlanningReadGate == nil, "The next planning read already has a gate") - nextPlanningReadGate = gate + func gateNextPlanningRead(with gate: PlanningOperationGate) { + precondition(nextPlanningOperationGate == nil, "The next planning read already has a gate") + nextPlanningOperationGate = gate } func failPlannedStays() { shouldFailPlannedStay = true } + func gateNextPlannedStayWrite(with gate: PlanningOperationGate) { + precondition(nextPlannedStayWriteGate == nil, "The next planning write already has a gate") + nextPlannedStayWriteGate = gate + } + /// Makes `samples(in:)` throw, so a `refresh()`'s year-report load fails and /// the model's `.failed` load state is exercisable. func failSamples() { @@ -204,6 +216,7 @@ actor TestStore: WhereStore { } func samples(in interval: DateInterval) async throws -> [LocationSample] { + sampleReadCount += 1 if shouldFailSamples { throw SampleReadFailure() } if gateFirstSamplesCall, !firstSamplesSeen { firstSamplesSeen = true @@ -345,8 +358,8 @@ actor TestStore: WhereStore { } func plannedStayRecords() async throws -> [PlannedStayRecord] { - let gate = nextPlanningReadGate - nextPlanningReadGate = nil + let gate = nextPlanningOperationGate + nextPlanningOperationGate = nil let result: Result<[PlannedStayRecord], Error> do { if shouldFailPlanningRead { throw PlannedStaySaveFailure() } @@ -372,6 +385,10 @@ actor TestStore: WhereStore { } func replacePlannedStayRecord(with record: PlannedStayRecord) async throws { + plannedStayWriteCount += 1 + let gate = nextPlannedStayWriteGate + nextPlannedStayWriteGate = nil + if let gate { await gate.suspend() } if shouldFailPlannedStay { throw PlannedStaySaveFailure() } try await backing.replacePlannedStayRecord(with: record) } From 2f7e92bebffe178e5ccceef961f58253362fe6fe Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Fri, 30 Oct 2026 18:06:27 -0400 Subject: [PATCH 02/18] Preserve calendar zoom and accessibility across Duo layouts --- Where/WhereUI/README.md | 1 + .../Sources/Primary/CalendarContentView.swift | 58 ++++++++++++------- .../Tests/CalendarContentViewTests.swift | 18 +++--- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index 8e45eb2d0..6fbc739a7 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -72,6 +72,7 @@ Demo mode hides this surface and performs no update checks. `MainTabs` owns one report model and one `SceneNavigationModel` for the session. The navigation model retains tab selection, region routes, the compact year mode, Settings routes, search, and calendar anchors. Each destination receives its own child state. Calendar, timeline, and Locations anchors survive pane remounts. +The calendar keeps its native scroll binding in the mounted view. Retained presentation state restores the typed month or estimate target after a remount. A report-year change waits for matching data before positioning. Stale visibility callbacks cannot replace the new year's anchor. Calendar sections and day rows measure eagerly so offscreen estimates cannot move a restored month. The timeline keeps its pinned overview outside the scrolling rows; large text places it in the content flow. diff --git a/Where/WhereUI/Sources/Primary/CalendarContentView.swift b/Where/WhereUI/Sources/Primary/CalendarContentView.swift index 78a30fa92..653743128 100644 --- a/Where/WhereUI/Sources/Primary/CalendarContentView.swift +++ b/Where/WhereUI/Sources/Primary/CalendarContentView.swift @@ -29,6 +29,8 @@ struct CalendarContentView: View { @State private var presentation: CalendarPresentationModel @AccessibilityFocusState private var focusedTarget: CalendarPresentationModel.Target? @State private var zoom = CalendarZoomTransitionModel() + @State private var calendarScrollPosition = ScrollPosition(idType: CalendarPresentationModel + .Target.self) @State private var scrollOffset = CGPoint.zero /// A nil persistence key keeps zoom session-only, including in previews. @@ -196,7 +198,6 @@ struct CalendarContentView: View { } private func calendarContent(months: [CalendarMonth]) -> some View { - @Bindable var navigation = navigation let availableMonths = CalendarPresentationModel.visibleMonths( months, report: report, @@ -273,7 +274,7 @@ struct CalendarContentView: View { } .id(presentation.mode) .transition(stylesheet.calendar.overview.transition) - .scrollPosition($navigation.calendarScrollPosition) + .scrollPosition($calendarScrollPosition) .onScrollGeometryChange(for: CGPoint.self) { CGPoint( x: $0.contentOffset.x + $0.contentInsets.leading, @@ -283,7 +284,7 @@ struct CalendarContentView: View { scrollOffset = value } .onScrollTargetVisibilityChange(idType: CalendarPresentationModel.Target.self) { targets in - if zoom.phase == nil, navigation.calendarScrollPosition.isPositionedByUser, + if zoom.phase == nil, calendarScrollPosition.isPositionedByUser, let first = targets.first { presentation.remember(first) navigation.recordVisibleMonths([first.month]) @@ -338,14 +339,21 @@ struct CalendarContentView: View { .map { CalendarPresentationModel.Target.estimate(monthKey($0)) } ?? target : target if zoom.phase == nil { - navigation.calendarScrollPosition.scrollTo( + calendarScrollPosition.scrollTo( id: initialTarget, anchor: isInitial ? .bottom : .top, ) if !isInitial { focusedTarget = target } } + } else if displayMode == .month, let month = navigation.displayedMonth, + month.year == report.selectedYear + { + calendarScrollPosition.scrollTo( + id: CalendarPresentationModel.Target.month(month), + anchor: .top, + ) } else { - navigation.calendarScrollPosition.scrollTo(edge: .top) + calendarScrollPosition.scrollTo(edge: .top) } } .onChange(of: navigation.revealRequest, initial: true) { _, _ in @@ -366,7 +374,7 @@ struct CalendarContentView: View { let value, value.mode == .detail, let target = presentation.target, value.frames[target] != nil { - navigation.calendarScrollPosition.scrollTo(id: target, anchor: .top) + calendarScrollPosition.scrollTo(id: target, anchor: .top) } } } @@ -396,7 +404,7 @@ struct CalendarContentView: View { else { return false } zoom.cancel() if displayMode == .year { presentation.expand(.month(request.month)) } - navigation.calendarScrollPosition.scrollTo( + calendarScrollPosition.scrollTo( id: CalendarPresentationModel.Target.month(request.month), anchor: .top, ) @@ -431,18 +439,17 @@ struct CalendarContentView: View { zoom.updateMeasurement(nil) switch presentation.mode { case .overview: - navigation.calendarScrollPosition = ScrollPosition( + calendarScrollPosition = ScrollPosition( idType: CalendarPresentationModel.Target.self, edge: .top, ) case .detail: if zoom.phase != nil { - navigation - .calendarScrollPosition = + calendarScrollPosition = ScrollPosition(idType: CalendarPresentationModel.Target .self) } else if let target = presentation.target { - navigation.calendarScrollPosition = ScrollPosition(id: target, anchor: .top) + calendarScrollPosition = ScrollPosition(id: target, anchor: .top) } } } @@ -557,6 +564,21 @@ struct CalendarContentView: View { } } +/// Adds default activation while preserving the month card's full accessibility frame. +private struct MonthSelectionAccessibility: ViewModifier { + let action: (() -> Void)? + + func body(content: Content) -> some View { + if let action { + content + .accessibilityAddTraits(.isButton) + .accessibilityAction { action() } + } else { + content + } + } +} + /// One month section: weekday header row, a day grid, and a footer tallying the /// days spent in each region that month. private struct MonthGridView: View { @@ -647,14 +669,10 @@ private struct MonthGridView: View { } } .contentShape(cardShape) - .accessibilityRepresentation { - if let selectMonth { - Button(monthAccessibility.label, action: selectMonth) - .accessibilityValue(monthAccessibility.value) - } else { - Text(monthAccessibility.label).accessibilityValue(monthAccessibility.value) - } - } + .accessibilityElement(children: .ignore) + .accessibilityLabel(monthAccessibility.label) + .accessibilityValue(monthAccessibility.value) + .modifier(MonthSelectionAccessibility(action: selectMonth)) } private var monthHeading: some View { @@ -670,7 +688,7 @@ private struct MonthGridView: View { } private var monthSummary: some View { - VStack(alignment: .leading, spacing: calendar.month.sectionSpacing) { + Group { if displayMode == .month, let focusedRegion { MonthFooter( totals: [month.regionTotals diff --git a/Where/WhereUI/Tests/CalendarContentViewTests.swift b/Where/WhereUI/Tests/CalendarContentViewTests.swift index bcc844fce..1a379aa16 100644 --- a/Where/WhereUI/Tests/CalendarContentViewTests.swift +++ b/Where/WhereUI/Tests/CalendarContentViewTests.swift @@ -30,7 +30,7 @@ struct CalendarContentViewTests { try await waitForCalendar("overview to load") { element("November", in: host.view) != nil && isEnabled("Show Months", in: host.view) } - for _ in 0 ..< 3 { + for iteration in 0 ..< 6 { try activate(#require(element("November", in: host.view))) if !reduceMotion { try await waitForCalendar("month transition to start") { @@ -38,7 +38,7 @@ struct CalendarContentViewTests { .contains(.notEnabled) == true } } - try await waitForCalendar("November to expand") { + try await waitForCalendar("November to expand on iteration \(iteration)") { isEnabled("Show Year", in: host.view) && isVisible("November", in: host.view, expanded: true) } @@ -119,10 +119,7 @@ struct CalendarContentViewTests { } return visit(root) } -} -@MainActor -struct CalendarContentViewTestsDuo { @Test func repeatedLinkedSelectionExpandsOverviewAndKeepsMonthTarget() async throws { let report = PreviewSupport.loadedYearReportModel() let history = HistoryNavigationModel() @@ -143,8 +140,7 @@ struct CalendarContentViewTestsDuo { history.select(month: month, dateCalendar: report.calendar) try await waitForAsync { history.calendar.pendingReveal == nil && presentation.mode == .detail - && history.calendar.calendarScrollPosition - .viewID(type: CalendarPresentationModel.Target.self) == .month(month) + && isVisible("February", in: host.view, expanded: true) } presentation.toggle() #expect(presentation.mode == .overview) @@ -187,7 +183,10 @@ struct CalendarContentViewTestsDuo { try await waitForAsync { navigation.initiallyPositionedYear == report.selectedYear } let previousYear = report.selectedYear - 1 await report.select(year: previousYear) - try await waitForAsync { navigation.initiallyPositionedYear == previousYear } + try await waitForAsync { + navigation.initiallyPositionedYear == previousYear + && isVisible("December", in: host.view, expanded: true) + } #expect(navigation.scrollPosition.viewID(type: CalendarDay.self) == CalendarDay( year: previousYear, month: 12, @@ -221,8 +220,7 @@ struct CalendarContentViewTestsDuo { .whereBroadwayRoot()) try await show(second) { _ in try await waitForAsync { - navigation.calendarScrollPosition - .viewID(type: CalendarPresentationModel.Target.self) == .month(browsed) + isVisible("February", in: second.view, expanded: true) } #expect(scene.locations.calendar.initiallyPositionedYear == nil) } From ff3c92183db8c51ef788d3efa2c1026033774bd6 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Fri, 30 Oct 2026 18:42:05 -0400 Subject: [PATCH 03/18] Capture update settings through the shared scrolling content --- Where/WhereUI/Sources/Settings/SettingsView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Where/WhereUI/Sources/Settings/SettingsView.swift b/Where/WhereUI/Sources/Settings/SettingsView.swift index 20bbf1b72..79ddc4f3a 100644 --- a/Where/WhereUI/Sources/Settings/SettingsView.swift +++ b/Where/WhereUI/Sources/Settings/SettingsView.swift @@ -236,7 +236,7 @@ private struct SettingsDestinationView: View { name: "UpdateAvailable", configurations: .fullContentPhoneLightDark, ) { - SettingsView(report: PreviewSupport.loadedYearReportModel()) + snapshotList(report: PreviewSupport.loadedYearReportModel()) .environment(PreviewSupport.loadedModel()) .environment(PreviewSupport.loadedSession()) .environment(InstallerPreview.model(.available)) @@ -245,7 +245,7 @@ private struct SettingsDestinationView: View { name: "LocalBuild", configurations: .fullContentPhoneLightDark, ) { - SettingsView(report: PreviewSupport.loadedYearReportModel()) + snapshotList(report: PreviewSupport.loadedYearReportModel()) .environment(PreviewSupport.loadedModel()) .environment(PreviewSupport.loadedSession()) .environment(InstallerPreview.model(.local)) From 57e1ac7168235a74ea25761554bfc6b487fddb88 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Fri, 30 Oct 2026 18:42:06 -0400 Subject: [PATCH 04/18] Refresh Duo integration snapshots after merging main --- .../adaptiveHistoryLab.outerPortrait_iPad.png | 4 ++-- .../adaptiveHistoryLab.tabletop_iPad.png | 4 ++-- .../calendarContent.HistoryColumn_historyColumn.png | 4 ++-- ...adStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png | 4 ++-- ...iveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png | 4 ++-- .../locations.BrowseColumn_browseColumn.png | 4 ++-- ...abs.Adaptive-Locations-book-Selected_duoInnerLandscape.png | 4 ++-- .../mainTabs.Adaptive-Locations-book_duoInnerLandscape.png | 4 ++-- ...bs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png | 4 ++-- ...tive-Locations-innerPortrait-Selected_duoInnerPortrait.png | 4 ++-- ...Tabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png | 4 ++-- ...Locations-innerPortrait_duoInnerPortrait_accessibility.png | 4 ++-- ...Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png | 4 ++-- .../mainTabs.Adaptive-Locations-narrow_narrowWindow.png | 4 ++-- ...Tabs.Adaptive-Locations-outerPortrait_duoOuterPortrait.png | 4 ++-- ....Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png | 4 ++-- ...ptive-Settings-innerPortrait-Selected_duoInnerPortrait.png | 4 ++-- ...mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png | 4 ++-- .../mainTabs.Adaptive-Year-book_duoInnerLandscape.png | 4 ++-- ...ainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png | 4 ++-- ....Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png | 4 ++-- ...tive-Year-innerPortrait_duoInnerPortrait_accessibility.png | 4 ++-- ...Tabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png | 4 ++-- .../mainTabs.Adaptive-Year-narrow_narrowWindow.png | 4 ++-- ...ainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png | 4 ++-- .../mainTabs.Adaptive-Year-outerPortrait_duoOuterPortrait.png | 4 ++-- ...adStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png | 4 ++-- 27 files changed, 54 insertions(+), 54 deletions(-) diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png index aa2a6b467..283944eff 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c53b88bed3f6a401e13162c1a36c2c57238a39b0054e55186915425eb5cc9cf6 -size 976512 +oid sha256:a0073363448f5e12fff9b7dff44e79a9972fae06992f3b0c94e6b16843d475b7 +size 964642 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png index 62871938c..9850fbe7b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed752548de92a34849f5799e89a67c9e515c832770d4e26f225ccb143d04973e -size 2186307 +oid sha256:118e0917d245942ae4d0f15cba28a1f5599e483c08ee6f50a48dac26cceffde2 +size 2160398 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.HistoryColumn_historyColumn.png b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.HistoryColumn_historyColumn.png index 06580a6cb..5d7143ce6 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.HistoryColumn_historyColumn.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.HistoryColumn_historyColumn.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:039ab62cd46ee03977c31f57073646ecbcaf398b68f2544ec7603d664084ce57 -size 1797558 +oid sha256:02bcf4a6ab03fc2df8a80286f19a21c97f68e3193bd8e2e111c2c2cfeee10516 +size 1775810 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png index a5b09f928..a8b811105 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db3db1190f4920032b163fdc9dc2abafd7f4a64da561103e9f91a733ab7c6f9f -size 604511 +oid sha256:bc5d5750616b469744c3e9c5ba26e4424ed0cc3aaa031f286e04e3a3a9c02f8d +size 592936 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png index 91864358b..ff49d740b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2935de9dfba620bfc0c3cb7622ff2dabc7511bf0402a7eede6f9e86256ceeae2 -size 511264 +oid sha256:da97b0464f939dca048364156edc20cf9d8cad4c359a5940193603cbb4960922 +size 501531 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.BrowseColumn_browseColumn.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.BrowseColumn_browseColumn.png index 10b2cba05..66d029bca 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.BrowseColumn_browseColumn.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.BrowseColumn_browseColumn.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61d783606c74c3039bdd25467ffd58584cb6048cd2c349cab0a8247ad30bddd6 -size 1484538 +oid sha256:d583262af0c8d4ab1263fc39044cec013d7e7d6466f69497325b2cf053326f74 +size 1383579 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png index 00340c259..70cb591cf 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a98b418b4765c8d619a50b254885eabd6d41228591e172831da88cc0742ad817 -size 2769656 +oid sha256:3f97c7b1ce88bb565e9c6ae5c56651ba9eba93cfc719067fdbe0a3925cf0ac7c +size 2755833 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png index 6d5df0f2c..24ff90322 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a30c09526929a833d63a1b1cd2f1ae069dc86f03f81230309ae7ecdcffb6e08 -size 2777708 +oid sha256:87b6b0407970f0a5406258e0de2bc074bad3a061a78b51ee4095c3e68f00eb1b +size 3642155 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png index 6d5df0f2c..24ff90322 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a30c09526929a833d63a1b1cd2f1ae069dc86f03f81230309ae7ecdcffb6e08 -size 2777708 +oid sha256:87b6b0407970f0a5406258e0de2bc074bad3a061a78b51ee4095c3e68f00eb1b +size 3642155 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png index 9a9adc287..a6bb30734 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43f96bcdb2de942df6ecdb1117561f757928c4c27505f44c508909f67bf0f495 -size 2705770 +oid sha256:ba8fa6221732372415144eb11bd527f19d7777692cee4a140aab5b9862107243 +size 2679304 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png index e30dcdfed..fafdb8175 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1ada5f99d1b8cb8ca0038f202cd765e9b02756ec88a00a579780489ad7be9fa -size 3114412 +oid sha256:3100dab50d5e034b2e902aa588670b6970208cd2c84d0e2c31142a2ec33ff98b +size 3132485 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_accessibility.png index b5f6270c8..2d528a6b7 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6aeaa2e827174d03fbd7b24d6662b591c61fcd1ea142c7757b7d10f632d847d -size 3399725 +oid sha256:5b8e4b215550d5a0194cb79f56278c0e032e6b47db6721115f78ff865c303a59 +size 3417322 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png index 8e069fee6..d929304e4 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f4cd5b8dc045ca65846c0dc8ce6b9dae9ea6ec8bb4dc347e65034aa6876ee8e -size 2291789 +oid sha256:a13970e6ad3d06c6754769d582bc170af8405e6da64553443e41060b7fea50bd +size 2347354 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-narrow_narrowWindow.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-narrow_narrowWindow.png index 75d2641dd..a222c5559 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-narrow_narrowWindow.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-narrow_narrowWindow.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92c14ee2a8fc6a1b406d4d9acd53b511d72c3775326320d3624bc1e42675b76b -size 1777621 +oid sha256:b159193223a7158a0ef9b8835af65181b8844dc991198a5a5061ebbced14d510 +size 1770395 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-outerPortrait_duoOuterPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-outerPortrait_duoOuterPortrait.png index 3cc1681aa..e82c8d0d0 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-outerPortrait_duoOuterPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-outerPortrait_duoOuterPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d0bda8f79942374a6f28b197f3a354bea1dfa1b164e2e4e1c8d6a464ed31621 -size 2684889 +oid sha256:14718aec7d6971df5effea7a85829558aad63a07e013b3ac690b417ce053ff1d +size 2667818 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png index 38536f1ff..3712c26b9 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee8a80e3947a9a165d41859d49777f886fee835437da732258765de6918ff71b -size 1799650 +oid sha256:46a42fa30de3b9b47f8e84e613209b24f7bafb72d88d3522e96b91456ee220e9 +size 1784390 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait-Selected_duoInnerPortrait.png index 09d488c2e..5d35512cb 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait-Selected_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait-Selected_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c542f155243936bb6c3e556eb71bf1973db2a10c2d6c0fe9c1ebc0a4aa2941d9 -size 2410888 +oid sha256:a379067f6cb0e049a498904ff0c15e9cda07e0cbffb71084e79dc4607fdcc36b +size 2325781 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png index 5f9a31574..3819c73d6 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72ce7962efe0adcbe5eaadc8da0794cff6ecda629adc0992464d3bea9a1eaf95 -size 1692834 +oid sha256:dbac042a0284a9db30e2996c19147121fed27d4c96ce818dc94a00173dbfeb7e +size 1686754 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png index 59489b05b..0cbaa6c1e 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5ebeb37eedbdefc3ef2129eb1a894ae0a7ad7bc79e5ad2f6a0cc54138e3a763 -size 2553279 +oid sha256:598325e66fd1a8588d4e76db49973efdfab3cda19a8a4845e120706c79c71b86 +size 2549417 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png index 59489b05b..0cbaa6c1e 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5ebeb37eedbdefc3ef2129eb1a894ae0a7ad7bc79e5ad2f6a0cc54138e3a763 -size 2553279 +oid sha256:598325e66fd1a8588d4e76db49973efdfab3cda19a8a4845e120706c79c71b86 +size 2549417 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png index fa91c8633..9a8d36218 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cb1843ee96ace4362668782f06a153f72ea073b8425162c2fb22e27b27559e2 -size 2157066 +oid sha256:7e507f70ef4b2945f30c0dbd6ce6c73ef4f462d811610df24fdc5cc8a056194b +size 2148691 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png index 4f7f0b7d9..a6f6b916b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34b912d250b61eebc3f8d5bfa0d2b4435ddd7d96394c1e04f118f70dcf37522a -size 3536699 +oid sha256:e0dc1a2bf1a03445874bb9f58531f86fe4c92c909e852d90a8b71ef21b1ada43 +size 3552085 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png index 47a5e0526..edc83205c 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a2a035970f409c5f6062a0b5c23fe6b710c0a5c77e9f59aacaace1899e5026b -size 2104359 +oid sha256:86421760ddc9b09a8cfa568cab811917a4157a4f7d8970bb01f90db1c2464cb3 +size 2121021 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-narrow_narrowWindow.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-narrow_narrowWindow.png index b5fb99217..aff7fccac 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-narrow_narrowWindow.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-narrow_narrowWindow.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7ab8ebe2b55ab14a8e68ec136920c6fbf3e3e6b1240d293ff23e79054a11166 -size 637706 +oid sha256:7ecf2a577a673859d9b1a3a628d19ceead50078da4ec8eb6b6ada64a2fa036a8 +size 1152201 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png index 8956893bb..ffb33d73b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e37cb5bc7e86e0549f602c8d241ce7229e0884c63787eebf1998a59df22a5ce9 -size 297141 +oid sha256:6ccbf93ebfe512bd93e4d497d950d50b86b966d171dd19a72880d409f08bf257 +size 1636022 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerPortrait_duoOuterPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerPortrait_duoOuterPortrait.png index fb2f2180a..7efd3c927 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerPortrait_duoOuterPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerPortrait_duoOuterPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f553698be31cb9980d9376f1b32782dc98a8d746f569651bf3fa9709859322e -size 851280 +oid sha256:20a3d90820d9f5d18f7841c60ea104289c807c49f2a4196fe01981704b201584 +size 1399384 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png index 3532566c8..d8ac6d78d 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af520b49a3e327c99c991388f223b15e3f977bdfd100f9dfd0e2f39d0ae93110 -size 1453009 +oid sha256:2570ac71042d9aa3b8ef8e05f0659633770e710d22b8d9b70743eb77545e4bd9 +size 1452037 From 564e1bee6bc2a53870b1e93bf1287f9828d38836 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Fri, 30 Oct 2026 19:52:07 -0400 Subject: [PATCH 05/18] Preserve initial estimate alignment across calendar remounts --- Where/WhereUI/README.md | 2 +- .../Model/CalendarPresentationModel.swift | 20 +++++++- .../Sources/Primary/CalendarContentView.swift | 11 ++-- .../Tests/CalendarContentViewTests.swift | 32 ++++++++++++ .../CalendarPresentationModelTests.swift | 51 +++++++++++++++++++ 5 files changed, 107 insertions(+), 9 deletions(-) diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index 6fbc739a7..c36ee34ee 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -72,7 +72,7 @@ Demo mode hides this surface and performs no update checks. `MainTabs` owns one report model and one `SceneNavigationModel` for the session. The navigation model retains tab selection, region routes, the compact year mode, Settings routes, search, and calendar anchors. Each destination receives its own child state. Calendar, timeline, and Locations anchors survive pane remounts. -The calendar keeps its native scroll binding in the mounted view. Retained presentation state restores the typed month or estimate target after a remount. +The calendar keeps its native scroll binding in the mounted view. Retained presentation state restores the typed month or estimate target after a remount. The initial bottom alignment survives remounts until browsing, selection, zoom, or a year change replaces it. A report-year change waits for matching data before positioning. Stale visibility callbacks cannot replace the new year's anchor. Calendar sections and day rows measure eagerly so offscreen estimates cannot move a restored month. The timeline keeps its pinned overview outside the scrolling rows; large text places it in the content flow. diff --git a/Where/WhereUI/Sources/Model/CalendarPresentationModel.swift b/Where/WhereUI/Sources/Model/CalendarPresentationModel.swift index c4f023a59..fb9614e1a 100644 --- a/Where/WhereUI/Sources/Model/CalendarPresentationModel.swift +++ b/Where/WhereUI/Sources/Model/CalendarPresentationModel.swift @@ -34,6 +34,8 @@ final class CalendarPresentationModel { } private(set) var target: Target? + /// Preserve the first bottom-aligned placement until navigation or browsing replaces it. + private(set) var initialBottomTarget: Target? private(set) var year: Int? private(set) var availableMonths: Set = [] private(set) var estimateMonth: CalendarDay? @@ -64,6 +66,9 @@ final class CalendarPresentationModel { target = (currentMonth.flatMap { self.availableMonths.contains($0) ? $0 : nil } ?? availableMonths.last).map(Target.month) } + if changedYear || initialBottomTarget.map({ !isAvailable($0) }) == true { + initialBottomTarget = nil + } if changedYear { rejectedTarget = nil } } @@ -74,8 +79,13 @@ final class CalendarPresentationModel { } } + func prepareInitialPosition() { + initialBottomTarget = estimateMonth.map(Target.estimate) ?? target + } + func remember(_ target: Target) { guard mode == .detail, isAvailable(target) else { return } + initialBottomTarget = nil self.target = target } @@ -87,6 +97,7 @@ final class CalendarPresentationModel { rejectionCount += 1 return false } + initialBottomTarget = nil target = destination mode = .detail return true @@ -94,7 +105,9 @@ final class CalendarPresentationModel { func toggle() { switch mode { - case .detail: mode = .overview + case .detail: + initialBottomTarget = nil + mode = .overview case .overview: expand(nil) } } @@ -102,7 +115,10 @@ final class CalendarPresentationModel { func finishPinch(magnification: CGFloat, destination: Target?) { switch mode { case .detail: - if magnification <= Self.overviewThreshold { mode = .overview } + if magnification <= Self.overviewThreshold { + initialBottomTarget = nil + mode = .overview + } case .overview: if magnification >= Self.detailThreshold { expand(destination) } } diff --git a/Where/WhereUI/Sources/Primary/CalendarContentView.swift b/Where/WhereUI/Sources/Primary/CalendarContentView.swift index 653743128..66225c820 100644 --- a/Where/WhereUI/Sources/Primary/CalendarContentView.swift +++ b/Where/WhereUI/Sources/Primary/CalendarContentView.swift @@ -335,15 +335,14 @@ struct CalendarContentView: View { if displayMode == .year, !overview, let target = presentation.target { - let initialTarget = isInitial ? estimateMonth - .map { CalendarPresentationModel.Target.estimate(monthKey($0)) } ?? target : - target + if isInitial { presentation.prepareInitialPosition() } + let initialBottomTarget = presentation.initialBottomTarget if zoom.phase == nil { calendarScrollPosition.scrollTo( - id: initialTarget, - anchor: isInitial ? .bottom : .top, + id: initialBottomTarget ?? target, + anchor: initialBottomTarget == nil ? .top : .bottom, ) - if !isInitial { focusedTarget = target } + if initialBottomTarget == nil { focusedTarget = target } } } else if displayMode == .month, let month = navigation.displayedMonth, month.year == report.selectedYear diff --git a/Where/WhereUI/Tests/CalendarContentViewTests.swift b/Where/WhereUI/Tests/CalendarContentViewTests.swift index 1a379aa16..a966422a3 100644 --- a/Where/WhereUI/Tests/CalendarContentViewTests.swift +++ b/Where/WhereUI/Tests/CalendarContentViewTests.swift @@ -195,6 +195,38 @@ struct CalendarContentViewTests { } } + @Test func aRemountedCalendarPreservesTheInitialEstimateAlignment() async throws { + let report = PreviewSupport.loadedYearReportModel() + let navigation = CalendarNavigationModel() + let first = UIHostingController(rootView: CalendarContentView( + report: report, + navigation: navigation, + ).whereBroadwayRoot()) + var initialBottom: CGFloat? + try await show(first) { _ in + try await waitForCalendar("initial estimate to reach the viewport bottom") { + guard let frame = element("Planned stays", in: first.view)?.accessibilityFrame + else { return false } + let viewport = first.view.convert(first.view.bounds, to: nil) + return viewport.contains(frame) && frame.minY > viewport.midY + } + initialBottom = element("Planned stays", in: first.view)?.accessibilityFrame.maxY + } + let expectedBottom = try #require(initialBottom) + let second = UIHostingController(rootView: CalendarContentView( + report: report, + navigation: navigation, + ).whereBroadwayRoot()) + try await show(second) { _ in + try await waitForCalendar("remounted estimate to retain its bottom alignment") { + guard let bottom = element("Planned stays", in: second.view)?.accessibilityFrame + .maxY + else { return false } + return abs(bottom - expectedBottom) <= 1 + } + } + } + @Test func aRemountedCalendarUsesTheSceneAnchor() async throws { let report = PreviewSupport.loadedYearReportModel() let scene = SceneNavigationModel() diff --git a/Where/WhereUI/Tests/CalendarPresentationModelTests.swift b/Where/WhereUI/Tests/CalendarPresentationModelTests.swift index 6757992bd..afbc1e92f 100644 --- a/Where/WhereUI/Tests/CalendarPresentationModelTests.swift +++ b/Where/WhereUI/Tests/CalendarPresentationModelTests.swift @@ -170,6 +170,57 @@ struct CalendarPresentationModelTests { #expect(!model.expand(nil)) } + @Test func initialEstimateAlignmentSurvivesReconciliationUntilSuperseded() { + let model = model() + model.prepareInitialPosition() + model.reconcile( + year: 2026, + availableMonths: (1 ... 10).map { month($0) }, + currentMonth: month(7), + estimateMonth: month(7), + ) + #expect(model.initialBottomTarget == .estimate(month(7))) + model.expand(.month(month(5))) + #expect(model.initialBottomTarget == nil) + #expect(model.target == .month(month(5))) + } + + @Test func browsingAndZoomReplaceInitialBottomAlignment() { + let model = model() + model.prepareInitialPosition() + model.remember(.month(month(5))) + #expect(model.initialBottomTarget == nil) + model.prepareInitialPosition() + model.toggle() + #expect(model.initialBottomTarget == nil) + model.expand(.estimate(month(7))) + #expect(model.initialBottomTarget == nil) + model.prepareInitialPosition() + model.finishPinch(magnification: 0.5, destination: nil) + #expect(model.initialBottomTarget == nil) + } + + @Test func removedEstimateAndNewYearDiscardInitialBottomAlignment() { + let model = model() + model.prepareInitialPosition() + model.reconcile( + year: 2026, + availableMonths: (1 ... 7).map { month($0) }, + currentMonth: month(7), + estimateMonth: nil, + ) + #expect(model.initialBottomTarget == nil) + model.prepareInitialPosition() + #expect(model.initialBottomTarget == .month(month(7))) + model.reconcile( + year: 2025, + availableMonths: [month(12, year: 2025)], + currentMonth: nil, + estimateMonth: nil, + ) + #expect(model.initialBottomTarget == nil) + } + @Test func overviewAvailabilityMatchesDetailedPlanningVisibility() throws { let report = PreviewSupport.plannedStayYearReportModel() let yearReport = try #require(report.report) From ab116024419345db439d26825c95255180df6081 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Fri, 30 Oct 2026 20:50:21 -0400 Subject: [PATCH 06/18] Save reviewed Duo capture updates and remaining validation gaps --- .../adaptiveHistoryLab.outerPortrait_iPad.png | 4 ++-- ...adStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png | 4 ++-- ...iveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png | 4 ++-- ...dStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png | 4 ++-- ...veLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png | 4 ++-- ...States.Adaptive-loading-innerPortrait_duoInnerPortrait.png | 4 ++-- ...eLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png | 4 ++-- ...abs.Adaptive-Locations-book-Selected_duoInnerLandscape.png | 4 ++-- .../mainTabs.Adaptive-Locations-book_duoInnerLandscape.png | 4 ++-- ...bs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png | 4 ++-- ...tive-Locations-innerPortrait-Selected_duoInnerPortrait.png | 4 ++-- ...Tabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png | 4 ++-- ....Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png | 3 --- ...tive-Locations-innerPortrait_duoInnerPortrait_contrast.png | 3 --- ...Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png | 4 ++-- ....Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png | 3 --- ....Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png | 4 ++-- .../mainTabs.Adaptive-Locations-tabletop_duoInnerPortrait.png | 4 ++-- ...s.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png | 3 --- ...ptive-Settings-innerPortrait_duoInnerPortrait_contrast.png | 3 --- ...s.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png | 3 --- ...mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png | 4 ++-- .../mainTabs.Adaptive-Year-book_duoInnerLandscape.png | 4 ++-- ...ainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png | 4 ++-- ....Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png | 4 ++-- .../mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png | 4 ++-- ...nTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png | 3 --- ....Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png | 3 --- ...Tabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png | 4 ++-- ...nTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png | 3 --- ...ainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png | 4 ++-- ...mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png | 4 ++-- ...nTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png | 4 ++-- .../mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png | 4 ++-- ....Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png | 3 +++ ...tive-Locations-innerPortrait_duoInnerPortrait_contrast.png | 3 +++ ....Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png | 3 +++ ...s.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png | 3 +++ ...ptive-Settings-innerPortrait_duoInnerPortrait_contrast.png | 3 +++ ...s.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png | 3 +++ ...nTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png | 3 +++ ....Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png | 3 +++ ...nTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png | 3 +++ 43 files changed, 77 insertions(+), 77 deletions(-) delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png index 283944eff..ddfa10ab8 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0073363448f5e12fff9b7dff44e79a9972fae06992f3b0c94e6b16843d475b7 -size 964642 +oid sha256:c849c5e72c9a537221d1998cf59126755b9c0d74e873eaa4524a803f2d8666c3 +size 1199545 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png index a8b811105..218f42c56 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc5d5750616b469744c3e9c5ba26e4424ed0cc3aaa031f286e04e3a3a9c02f8d -size 592936 +oid sha256:a07adb95aff53b39a138f8c1b3a9be4d26c76ff3c1ce90247ee2f4421c471549 +size 772886 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png index ff49d740b..64002fce0 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da97b0464f939dca048364156edc20cf9d8cad4c359a5940193603cbb4960922 -size 501531 +oid sha256:ce250167b7af378729603f0060e6cce9954df01a5fc0ddf672a6c1533e706302 +size 503973 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png index b2cc0838f..f9ac1cdec 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f59c4e1a48c974527e4cf787b69a2765f7900758ef0ead0012097224642058df -size 409110 +oid sha256:dcb4c15fa03e4474c842be07d1a09fab68a22a005abe3220a6d55cd9d32172fb +size 427515 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png index 119514268..37115c539 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98245c8a9b51013c4afb0ad8fa380cb547643c791547f6e67ac117d6e87dff91 -size 447197 +oid sha256:672b76d0dfcf9f78561c2b40884ca9615f516e2226684dd3f0c45484f9ea9caf +size 449749 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png index cb01f46d2..12e36dfce 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78003267cd09c76eb61d1b86976fc1812f3132f99f12622b7ad4238a8902962c -size 675474 +oid sha256:d8b81f953b3dd71b6b82d4ce4379d2322d4b6a89fe375d9cbdb60bf6fbf589f1 +size 696448 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png index 7abac70c8..b8b14d4c7 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d64fe4a98304df550302df4fe6d291eb28115d30aed9f752f030d6a91a9f36f4 -size 731860 +oid sha256:67e0025b08bb66aaa17d339612ac4f9dbff612fdc933650e0820a9ec506e0b9b +size 733257 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png index 70cb591cf..6484bb8f4 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f97c7b1ce88bb565e9c6ae5c56651ba9eba93cfc719067fdbe0a3925cf0ac7c -size 2755833 +oid sha256:40dbd11f1e6af07fa9510f0f30ee7c0d7d5ed6a7417382e2615f1de3681bf01b +size 3348631 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png index 24ff90322..b67e89076 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87b6b0407970f0a5406258e0de2bc074bad3a061a78b51ee4095c3e68f00eb1b -size 3642155 +oid sha256:e044768d9df31750d36b92667e475e6fdce1f6c00e7acc1c82b4000cc18711dd +size 3970138 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png index 24ff90322..0206da3ad 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87b6b0407970f0a5406258e0de2bc074bad3a061a78b51ee4095c3e68f00eb1b -size 3642155 +oid sha256:d2bb85f6de24c908d3df82607db3b6d38c3364a081a7e2a9714b8550941d9d64 +size 3970148 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png index a6bb30734..d55ec2e2c 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba8fa6221732372415144eb11bd527f19d7777692cee4a140aab5b9862107243 -size 2679304 +oid sha256:118279d65ddf5d4a5b2908ccfe7936d031eab39e2610f73ac84428ab441f5924 +size 3139081 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png index fafdb8175..0e77c92f2 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3100dab50d5e034b2e902aa588670b6970208cd2c84d0e2c31142a2ec33ff98b -size 3132485 +oid sha256:d717e9eedc4a4222d55c435ab630c794ecdf1fe44a1d24e278234a89e6b4330c +size 3504333 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png deleted file mode 100644 index 73d920d13..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:56665e9f5030ecf5b6533809018622e4ffba4a4f30b5d84580ec0f21351e028c -size 4264228 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png deleted file mode 100644 index a0a232839..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2a5f0c67ef577ca5f16acfcd6307a6ade4f21cc19f5a836d52b50439e32f8ff8 -size 3107305 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png index d929304e4..78715c780 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a13970e6ad3d06c6754769d582bc170af8405e6da64553443e41060b7fea50bd -size 2347354 +oid sha256:844b5beda8ae6e7d0e23c5cfc85d94431d87e8a1f1e2e3f8e41575e6208a6be7 +size 3133899 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png deleted file mode 100644 index d841f9be6..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5f0b7783770bc16582982976dc8033e2e7a4fd29501d5bd5c72d2715d1e5e34e -size 3108137 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png index 3712c26b9..f9263e932 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46a42fa30de3b9b47f8e84e613209b24f7bafb72d88d3522e96b91456ee220e9 -size 1784390 +oid sha256:70a809fe61480e2d150a1d9215707f97fa97f13c35960ed6b3f215cf80a83529 +size 1953622 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop_duoInnerPortrait.png index 0430f6cd1..6d4549592 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a239ae3a58fc4b79bedf3b3a7255b41817ccea5fa2df548bda626ebd730dafa2 -size 1813966 +oid sha256:a964726b036fbfd92bbcd626e62ebce71747aad2d3f1df6ca5846bc0973d4ffc +size 1975625 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png deleted file mode 100644 index 33ef723ea..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2b6975622141c150cfac5ee3a9f6aad12ae94a5c5239749a76bd1cdba7028f83 -size 313563 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png deleted file mode 100644 index 3469ab77d..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1c01437248dba8e0795874375379d322decdbab1a7ad4e2066c6f15ebc37835e -size 1439418 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png deleted file mode 100644 index fafb26276..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bc326805c08f2add9977467c021666ab07bcb2028ce27759b8c65285096c598b -size 1414387 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png index 3819c73d6..a0c654d19 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbac042a0284a9db30e2996c19147121fed27d4c96ce818dc94a00173dbfeb7e -size 1686754 +oid sha256:a04a9e9a7794ea9956b012ab1d61f6a7f8afbf1402b925ef2d3708c9a74c7793 +size 755178 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png index 0cbaa6c1e..4da74317b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:598325e66fd1a8588d4e76db49973efdfab3cda19a8a4845e120706c79c71b86 -size 2549417 +oid sha256:add2032d21b9f861b07d17765cbfaaab6b4f756d8bfa18e70c3eed039414d219 +size 2614152 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png index 0cbaa6c1e..f97852f3f 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:598325e66fd1a8588d4e76db49973efdfab3cda19a8a4845e120706c79c71b86 -size 2549417 +oid sha256:2610ca4762603726dcede826c389c36f89c11b58dcad416989876b88ef11788f +size 2614150 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png index 9a8d36218..7e783f7ad 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e507f70ef4b2945f30c0dbd6ce6c73ef4f462d811610df24fdc5cc8a056194b -size 2148691 +oid sha256:03400ef739502acd2b54b401bbfbecf303c3e3e9b56d7ba3851301e8151e8bff +size 835702 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png index d14061e92..55c5798b0 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19970a43c73b0512fac628d00dfed850062e1a3957dab8710e2da8826980bcd5 -size 3334286 +oid sha256:93853e4e1fcddc91942ec66af135ff4fe035f4f9f714c8379ebe2675f9c27ab2 +size 2289728 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png deleted file mode 100644 index f5bbadf03..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9e5e8f6ba98b55b56d460bdd2fc3e72bfb719e0792b6d8b9c96c0b6535f3df31 -size 1186574 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png deleted file mode 100644 index dcef53106..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:efbdca2fab2ac4fa090c7d78dfdd79bd5e4609649ec5ff82f5a59dc22e108ced -size 3343086 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png index edc83205c..c1fd1d273 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86421760ddc9b09a8cfa568cab811917a4157a4f7d8970bb01f90db1c2464cb3 -size 2121021 +oid sha256:c31ff6f711d02e45c9cc2f82427f6940f1c04ef73d8b9728b483b089b3f75e33 +size 2708749 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png deleted file mode 100644 index 15fb6684c..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0ed1b025bb6e46b9a838dc67d39f99673d2cc931eb074418ad78c35721d5dba6 -size 3035516 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png index ffb33d73b..3a43dd572 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ccbf93ebfe512bd93e4d497d950d50b86b966d171dd19a72880d409f08bf257 -size 1636022 +oid sha256:1307784675a873167fe8b8a7a486d039eb110819e3f0bcd0427d7611f8651cc4 +size 1621240 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png index 52042651b..9cba6d909 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b36d1ebb49dceed7c5303104da2a3382d67e3afe0068bd517cfa8dd8f86f64c9 -size 723826 +oid sha256:79a3868f0d747fcef8b180961e687188a7a341ff0e5784cfdb3fd37026ca529f +size 684781 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png index e7f6cd045..0e483ba55 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6149d03f45bf3f9d1e55e66aa6a03e8d1f7c0700fbd4889a4191c642bf2dcb2 -size 462966 +oid sha256:0f93a0dd7ae3820b88e107c3b27198794769eb6b513441ce9aaab378c8f93f13 +size 476078 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png index cdf5a00a6..4d118d330 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7428ac6d498ea4daac726ef3845b44a406a9af7ac5425754e621ffcdfc343ad3 -size 584230 +oid sha256:6964a1229a9838cae95eaf8624be934f9819f8fe4595dfdf91ebd58bbeac03d3 +size 615374 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png new file mode 100644 index 000000000..10fb8cea4 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8d3333e4170a24dfb059e6318ec5dbd82500e5bc7961957f9ce2c45b0d06fcc +size 4435476 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png new file mode 100644 index 000000000..f59d0aa9e --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:316cdaa178c4eced3c5b38bef0cacede78324d70f9f47d85088047fe007cc7cc +size 3471495 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png new file mode 100644 index 000000000..01353bdcd --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ee5c8eea91fe4bea0b45c524deb26741eb9fc7c73f249f9aa55da209a8a3b75 +size 3339070 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png new file mode 100644 index 000000000..1018d4b32 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2fb615a8077b3b509d88d813eefdd68eb594ed112542c3b3c301c56deaf2ec9 +size 319163 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png new file mode 100644 index 000000000..5a0e5d170 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9152d8cfc7fd2e463235bad2f279ff946c5f5a0c526075318ff9dc0678cb471 +size 1330322 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png new file mode 100644 index 000000000..433c6b738 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b672469cbc433cd04793c1139fed6dbb15d137499ea4145729f772d5991e27a +size 1181073 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png new file mode 100644 index 000000000..f76a390b3 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b9691b355011ac2dae317e807c9877c7794f8848267b4146746bb7df65b4b56 +size 3647843 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png new file mode 100644 index 000000000..8edc835e6 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1872aeb51172821bc722b7f011c7a15e42729138bbe3741b87728d443e377540 +size 2328517 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png new file mode 100644 index 000000000..5700cf7e3 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3235cd87b6a3c9ffb8c022f887fe64935f28641a66c837a377daaa2d1e7d4899 +size 2297555 From fe85368e9e20a0ace3a1478966cb8a10b436dc25 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Mon, 9 Nov 2026 16:06:39 -0500 Subject: [PATCH 07/18] Read current window insets when delivering developer overlay clearance --- .../Developer/DeveloperTabBarInset.swift | 15 +++-- .../Tests/DeveloperTabBarInsetTests.swift | 63 +++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/Where/WhereUI/Sources/Developer/DeveloperTabBarInset.swift b/Where/WhereUI/Sources/Developer/DeveloperTabBarInset.swift index 4e6c8f42a..5c64e67f8 100644 --- a/Where/WhereUI/Sources/Developer/DeveloperTabBarInset.swift +++ b/Where/WhereUI/Sources/Developer/DeveloperTabBarInset.swift @@ -101,7 +101,7 @@ extension View { } /// Reads the window containing this reporter. It never searches other scenes or screens. - private struct DeveloperWindowInsetsReader: UIViewRepresentable { + struct DeveloperWindowInsetsReader: UIViewRepresentable { let changed: (UIEdgeInsets) -> Void func makeUIView(context _: Context) -> InsetView { @@ -115,6 +115,7 @@ extension View { static func dismantleUIView(_ view: InsetView, coordinator _: Void) { view.pending?.cancel() + view.pending = nil view.changed = nil } @@ -139,10 +140,16 @@ extension View { } func scheduleUpdate() { - guard let insets = window?.safeAreaInsets, reported != insets else { return } - pending?.cancel() + guard changed != nil, window != nil, pending == nil else { return } pending = Task { @MainActor [weak self] in - guard !Task.isCancelled, let self, window != nil else { return } + // Dismantling already clears the cancelled task's handle. Do + // not let it clear a newer update if this view is reused. + guard !Task.isCancelled, let self else { return } + defer { pending = nil } + // UIKit can restore transient geometry before this deferred + // delivery runs. Publish the current window, not the sample + // that originally requested an update. + guard let insets = window?.safeAreaInsets, reported != insets else { return } reported = insets changed?(insets) } diff --git a/Where/WhereUI/Tests/DeveloperTabBarInsetTests.swift b/Where/WhereUI/Tests/DeveloperTabBarInsetTests.swift index 1c274a86e..7d4087a2a 100644 --- a/Where/WhereUI/Tests/DeveloperTabBarInsetTests.swift +++ b/Where/WhereUI/Tests/DeveloperTabBarInsetTests.swift @@ -6,6 +6,59 @@ import UIKit @MainActor struct DeveloperTabBarInsetTests { + @Test func windowReaderDiscardsTransientInsetsBeforeDeferredDelivery() async throws { + let window = InsetsWindow(frame: CGRect(x: 0, y: 0, width: 400, height: 800)) + let view = DeveloperWindowInsetsReader.InsetView() + var reported: [UIEdgeInsets] = [] + view.changed = { reported.append($0) } + window.addSubview(view) + let initial = try #require(view.pending) + await initial.value + #expect(reported == [.zero]) + + window.simulatedInsets.bottom = 34 + view.scheduleUpdate() + let pending = try #require(view.pending) + // Restore the last published geometry before the task can execute. + // The second request must invalidate the transient 34pt observation. + window.simulatedInsets = .zero + view.scheduleUpdate() + await pending.value + #expect(reported == [.zero]) + #expect(view.pending == nil) + + window.simulatedInsets.bottom = 20 + view.scheduleUpdate() + let next = try #require(view.pending) + await next.value + #expect(reported == [.zero, UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0)]) + #expect(view.pending == nil) + } + + @Test(arguments: [false, true]) + func windowReaderDropsPendingDeliveryWhenRemoved(dismantles: Bool) async throws { + let window = InsetsWindow(frame: CGRect(x: 0, y: 0, width: 400, height: 800)) + let view = DeveloperWindowInsetsReader.InsetView() + var reported: [UIEdgeInsets] = [] + view.changed = { reported.append($0) } + window.addSubview(view) + let initial = try #require(view.pending) + await initial.value + #expect(reported == [.zero]) + + window.simulatedInsets.bottom = 34 + view.scheduleUpdate() + let pending = try #require(view.pending) + if dismantles { + DeveloperWindowInsetsReader.dismantleUIView(view, coordinator: ()) + } else { + view.removeFromSuperview() + } + await pending.value + #expect(reported == [.zero]) + #expect(view.pending == nil) + } + @Test func reduceKeepsEachEdgesLargestReportedInset() { var value = DeveloperTabBarInsetKey.defaultValue DeveloperTabBarInsetKey.reduce(value: &value) { EdgeInsets( @@ -63,6 +116,16 @@ struct DeveloperTabBarInsetTests { } } +/// Supplies deterministic native-window changes without requiring a simulator scene. +@MainActor +private final class InsetsWindow: UIWindow { + var simulatedInsets = UIEdgeInsets.zero + + override var safeAreaInsets: UIEdgeInsets { + simulatedInsets + } +} + /// Bridges the `@Sendable` `onPreferenceChange` callback back to the test. Reads /// and writes are confined to the main actor by the hosted run loop. private final class InsetBox: @unchecked Sendable { From e1a4e1dcc3556f333586cd0ba4cc21f2ae23b44a Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Mon, 9 Nov 2026 16:53:11 -0500 Subject: [PATCH 08/18] Preserve Duo lab navigation with scene-backed pose previews --- .../Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift index cd77c3e53..25564d12b 100644 --- a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift +++ b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift @@ -66,7 +66,7 @@ .environment(world.session) .environment(\.isInDemoMode, true) .environment(\.demoExitPresentation, .embedded) - .padding(EdgeInsets( + .safeAreaPadding(EdgeInsets( top: insets.top, leading: insets.leading, bottom: insets.bottom, @@ -100,6 +100,7 @@ verticalSizeClass: .regular, interfaceIdiom: .pad, ), + scenePolicy: .retained, ))], onReadyToSnapshot: { await fixture.prepare() }, ) { From dd5e30eae758b4857bcb24ad0fefe830d34bc7aa Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Wed, 11 Nov 2026 13:36:49 -0500 Subject: [PATCH 09/18] Keep adaptive capture fixes with the app integration Move the native Settings search placement, fold regression, and 101 reviewed integration references out of the follow-up PR. Refresh four scene-backed developer overlay references for the adaptive geometry and Duo Lab row. Validation: full iOS unit suite passed with 2,975 executed tests and no unexpected failures; host macros, architecture, and formatting passed. Adaptive lab, MainTabs, and Settings snapshots passed. All six adaptive developer-overlay captures repeat byte-for-byte after review. --- ...nspectorSurfaces.SwiftData_iPhone_dark.png | 4 +- Where/WhereUI/README.md | 1 + .../adaptiveHistoryLab.outerPortrait_iPad.png | 4 +- .../adaptiveHistoryLab.tabletop_iPad.png | 4 +- ...darContent.HistoryColumn_historyColumn.png | 4 +- ...loperOverlay.MenuBottomTrailing_iPhone.png | 4 +- ...Overlay.MenuBottomTrailing_iPhone_dark.png | 4 +- ...developerOverlay.MenuTopLeading_iPhone.png | 4 +- .../developerOverlay.SelectedTool_iPhone.png | 4 +- ...e-empty-innerPortrait_duoInnerPortrait.png | 4 +- ...aptive-empty-tabletop_duoInnerPortrait.png | 4 +- ...-failed-innerPortrait_duoInnerPortrait.png | 4 +- ...ptive-failed-tabletop_duoInnerPortrait.png | 4 +- ...loading-innerPortrait_duoInnerPortrait.png | 4 +- ...tive-loading-tabletop_duoInnerPortrait.png | 4 +- ...ations-book-Selected_duoInnerLandscape.png | 4 +- ...ptive-Locations-book_duoInnerLandscape.png | 4 +- ...tions-innerLandscape_duoInnerLandscape.png | 4 +- ...nnerPortrait-Selected_duoInnerPortrait.png | 4 +- ...cations-innerPortrait_duoInnerPortrait.png | 4 +- ...ortrait_duoInnerPortrait_accessibility.png | 4 +- ...ns-innerPortrait_duoInnerPortrait_dark.png | 4 +- ...ons-tabletop-Selected_duoInnerPortrait.png | 4 +- ...ve-Locations-tabletop_duoInnerPortrait.png | 4 +- ...aptive-Settings-book_duoInnerLandscape.png | 4 +- ...tings-innerLandscape_duoInnerLandscape.png | 4 +- ...nnerPortrait-Selected_duoInnerPortrait.png | 4 +- ...ettings-innerPortrait_duoInnerPortrait.png | 4 +- ...ortrait_duoInnerPortrait_accessibility.png | 4 +- ...gs-innerPortrait_duoInnerPortrait_dark.png | 4 +- ...tings-outerLandscape_duoOuterLandscape.png | 4 +- ...ive-Settings-tabletop_duoInnerPortrait.png | 4 +- ...e-Year-book-Selected_duoInnerLandscape.png | 4 +- ...s.Adaptive-Year-book_duoInnerLandscape.png | 4 +- ...-Year-innerLandscape_duoInnerLandscape.png | 4 +- ...nnerPortrait-Selected_duoInnerPortrait.png | 4 +- ...ve-Year-innerPortrait_duoInnerPortrait.png | 4 +- ...ortrait_duoInnerPortrait_accessibility.png | 4 +- ...ar-innerPortrait_duoInnerPortrait_dark.png | 4 +- ...-Year-outerLandscape_duoOuterLandscape.png | 4 +- ...e-Year-tabletop-Month_duoInnerPortrait.png | 4 +- ...ear-tabletop-Selected_duoInnerPortrait.png | 4 +- ...daptive-Year-tabletop_duoInnerPortrait.png | 4 +- ...s.UpdateAvailable_iPhone_accessibility.png | 4 +- ...omeActionRequired_iPhone_accessibility.png | 4 +- ...s.WelcomeLocating_iPhone_accessibility.png | 4 +- ...ons-innerPortrait_duoInnerPortrait_ax5.png | 4 +- ...nnerPortrait_duoInnerPortrait_contrast.png | 4 +- ...ons-innerPortrait_duoInnerPortrait_rtl.png | 4 +- ...ngs-innerPortrait_duoInnerPortrait_ax5.png | 4 +- ...nnerPortrait_duoInnerPortrait_contrast.png | 4 +- ...ngs-innerPortrait_duoInnerPortrait_rtl.png | 4 +- ...ear-innerPortrait_duoInnerPortrait_ax5.png | 4 +- ...nnerPortrait_duoInnerPortrait_contrast.png | 4 +- ...ear-innerPortrait_duoInnerPortrait_rtl.png | 4 +- ...presenceTimeline.Flights_iPad_contrast.png | 4 +- ...esenceTimeline.Flights_iPhone_contrast.png | 4 +- ...resenceTimeline.WithData_iPad_contrast.png | 4 +- ...senceTimeline.WithData_iPhone_contrast.png | 4 +- ...enceTimeline.BrowseColumn_browseColumn.png | 4 +- .../presenceTimeline.Flights_iPad.png | 4 +- ...nceTimeline.Flights_iPad_accessibility.png | 4 +- .../presenceTimeline.Flights_iPad_dark.png | 4 +- .../presenceTimeline.Flights_iPhone.png | 2 +- ...eTimeline.Flights_iPhone_accessibility.png | 4 +- .../presenceTimeline.Flights_iPhone_dark.png | 4 +- .../presenceTimeline.InitialBottom_iPhone.png | 4 +- ...enceTimeline.InitialBottom_iPhone_dark.png | 4 +- ...resenceTimeline.InterruptedStay_iPhone.png | 4 +- ...ceTimeline.InterruptedStay_iPhone_dark.png | 4 +- ...ne.PlannedStayAfterRecordingGap_iPhone.png | 4 +- ...annedStayAfterRecordingGap_iPhone_dark.png | 4 +- ....UncertainArrival_iPhone_accessibility.png | 4 +- .../presenceTimeline.WithData_iPad.png | 4 +- ...ceTimeline.WithData_iPad_accessibility.png | 4 +- .../presenceTimeline.WithData_iPad_dark.png | 4 +- .../presenceTimeline.WithData_iPhone.png | 4 +- .../presenceTimeline.WithData_iPhone_dark.png | 4 +- .../other/settings.Default_iPad_ax5.png | 4 +- .../other/settings.Default_iPad_contrast.png | 4 +- .../other/settings.Default_iPhone_ax5.png | 4 +- .../settings.Default_iPhone_contrast.png | 4 +- .../other/settings.Default_iPhone_rtl.png | 4 +- .../settings.BrowseColumn_browseColumn.png | 4 +- .../settings.Default_iPad.png | 4 +- .../settings.Default_iPad_accessibility.png | 4 +- .../settings.Default_iPad_dark.png | 4 +- .../settings.Default_iPhone.png | 4 +- .../settings.Default_iPhone_accessibility.png | 4 +- .../settings.Default_iPhone_dark.png | 4 +- .../settings.DemoMode_iPhone.png | 4 +- .../settings.DemoMode_iPhone_dark.png | 4 +- .../settings.LocalBuild_iPhone.png | 4 +- .../settings.LocalBuild_iPhone_dark.png | 4 +- ...s.RecordingConfigurationWarning_iPhone.png | 4 +- ...ordingConfigurationWarning_iPhone_dark.png | 4 +- .../settings.UpdateAvailable_iPhone.png | 4 +- .../settings.UpdateAvailable_iPhone_dark.png | 4 +- ...e-empty-innerPortrait_duoInnerPortrait.png | 4 +- ...aptive-empty-tabletop_duoInnerPortrait.png | 4 +- ...-failed-innerPortrait_duoInnerPortrait.png | 4 +- ...ptive-failed-tabletop_duoInnerPortrait.png | 4 +- ...loading-innerPortrait_duoInnerPortrait.png | 4 +- ...tive-loading-tabletop_duoInnerPortrait.png | 4 +- .../year.Loaded_iPad_accessibility.png | 4 +- .../year.Loaded_iPhone_accessibility.png | 4 +- .../Sources/Settings/SettingsContent.swift | 1 + Where/WhereUI/Tests/MainTabsTests.swift | 42 +++++++++++++++++++ 108 files changed, 253 insertions(+), 209 deletions(-) diff --git a/Shared/Inspector/SnapshotTests/__Snapshots__/InspectorSnapshotTests/inspectorSurfaces.SwiftData_iPhone_dark.png b/Shared/Inspector/SnapshotTests/__Snapshots__/InspectorSnapshotTests/inspectorSurfaces.SwiftData_iPhone_dark.png index 59de6e964..ba92613f5 100644 --- a/Shared/Inspector/SnapshotTests/__Snapshots__/InspectorSnapshotTests/inspectorSurfaces.SwiftData_iPhone_dark.png +++ b/Shared/Inspector/SnapshotTests/__Snapshots__/InspectorSnapshotTests/inspectorSurfaces.SwiftData_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09c9d0c095894dcfbc3001706e3937ed05e2d53cbb411988cf1f017c612d9dcc -size 147618 +oid sha256:fe78cdcc30b9006fc61ec64e83cc639b6aa417fd586bb60d2c1b9853f7d75d60 +size 129795 diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index c36ee34ee..b800537c0 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -83,6 +83,7 @@ selection reveals the first overlapping displayed entry. On a wide window, Locations shows cards beside the selected region's calendar. Without a region selection, the detail pane shows the year calendar. Your Year shows the calendar and timeline together. Settings uses native list and detail navigation. +Settings keeps its native search field visible in the navigation drawer across compact and split layouts. Narrow windows and accessibility text sizes use one pane. The compact calendar/timeline choice remains stored while both panes are visible. Theme choices stack when the available pane cannot fit two readable card specimens. diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png index ddfa10ab8..87fc3334d 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c849c5e72c9a537221d1998cf59126755b9c0d74e873eaa4524a803f2d8666c3 -size 1199545 +oid sha256:474d67e94b6fd33ad1a9562de96ffc93d21a24912ca97abd3af15d52dc082941 +size 1218527 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png index 9850fbe7b..112481b92 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:118e0917d245942ae4d0f15cba28a1f5599e483c08ee6f50a48dac26cceffde2 -size 2160398 +oid sha256:893bde30e63fddfa20a98b34bb3a837b6248349580f75e1415769efad9f13b66 +size 2583864 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.HistoryColumn_historyColumn.png b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.HistoryColumn_historyColumn.png index 5d7143ce6..10dd22948 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.HistoryColumn_historyColumn.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.HistoryColumn_historyColumn.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02bcf4a6ab03fc2df8a80286f19a21c97f68e3193bd8e2e111c2c2cfeee10516 -size 1775810 +oid sha256:da09278c1d6d2b52ee9e6e6643fcf304cb5471614bbc13904ce63de2a7fb024d +size 1772507 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuBottomTrailing_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuBottomTrailing_iPhone.png index b74268d0c..8349f73fb 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuBottomTrailing_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuBottomTrailing_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a2e64e94bc70d3c3dc81b0c14a2cc5f101faace8ef7792c993e3b85670d678b -size 430194 +oid sha256:cd65da3e3f6b54c692498b78e5ea15015d3c36d442f66366753ec63790a848f0 +size 464359 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuBottomTrailing_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuBottomTrailing_iPhone_dark.png index 13e15ffe7..dcfe0cb05 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuBottomTrailing_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuBottomTrailing_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e574d2318939c58669917d565a1e78d0d8cf59d89d61f5d7fe998d6067645a7 -size 339077 +oid sha256:68eb28273e9b93041a7fcf2ef929c3fae17bffbf6f3a099bdf23bdd3dfd806bc +size 362092 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuTopLeading_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuTopLeading_iPhone.png index 355f50682..d7e33ea69 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuTopLeading_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuTopLeading_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a49087f387a62dd51d76ba837b7f07c10dd01f6f99cbfd2c7460d26fbc4d1362 -size 431882 +oid sha256:b5817ef4d57a04951eb11d2c892723590c80a7e2e4a0aa28628cd597cae2b5e2 +size 466276 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.SelectedTool_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.SelectedTool_iPhone.png index fb0ff4571..af3d3e66b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.SelectedTool_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.SelectedTool_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad660fed56810947ac18399a308695fd6ef5eb06046290787f8ff051ba5a19d0 -size 176273 +oid sha256:9b50f950544ba23eb768cf7df0f46b56e4ad4da5eb7833b4d5092c5c7e091d6a +size 175860 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png index 218f42c56..75e6f4acc 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a07adb95aff53b39a138f8c1b3a9be4d26c76ff3c1ce90247ee2f4421c471549 -size 772886 +oid sha256:540d5c92ecde3f08188285cdedd4855cee63efd22397cd449447f1ad9914e6a1 +size 770995 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png index 64002fce0..998b4ea60 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce250167b7af378729603f0060e6cce9954df01a5fc0ddf672a6c1533e706302 -size 503973 +oid sha256:026c50e4c41e07db45230efaa2b0c99db230a430cb97dafbd00c7ecfa1a6d694 +size 501500 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png index f9ac1cdec..b3bd6ba67 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dcb4c15fa03e4474c842be07d1a09fab68a22a005abe3220a6d55cd9d32172fb -size 427515 +oid sha256:84faf3a5121ffe6e379b457a8fa8ef7724c176080610888eb028a51e9f259b95 +size 424287 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png index 37115c539..ec68fa377 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:672b76d0dfcf9f78561c2b40884ca9615f516e2226684dd3f0c45484f9ea9caf -size 449749 +oid sha256:74b6e8b788ad43f9853887289fb5ad958da79f1a40a8abc28280caeb6baeb6b1 +size 447244 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png index 12e36dfce..be5819d98 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8b81f953b3dd71b6b82d4ce4379d2322d4b6a89fe375d9cbdb60bf6fbf589f1 -size 696448 +oid sha256:cf5eda368453d24723c1fc83a76bca0b3c7540d0a53b488894d173582296f44d +size 692694 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png index b8b14d4c7..055125e11 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67e0025b08bb66aaa17d339612ac4f9dbff612fdc933650e0820a9ec506e0b9b -size 733257 +oid sha256:30595c1c21e3371b423c768475bfae5cbd48b3594a8ccc9e40421b7b384bd8ca +size 730843 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png index 6484bb8f4..30b64fcaf 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book-Selected_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40dbd11f1e6af07fa9510f0f30ee7c0d7d5ed6a7417382e2615f1de3681bf01b -size 3348631 +oid sha256:cea5710b0856817e0695e8ad2271b01b50fcd49bfea210a661e6b2090d2ed8dd +size 3325913 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png index b67e89076..15aff5013 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-book_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e044768d9df31750d36b92667e475e6fdce1f6c00e7acc1c82b4000cc18711dd -size 3970138 +oid sha256:d3ee222f788c1da3eaf2edcad0982546974d0f45522ffdca0ab49bd4ad7d88e9 +size 4032340 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png index 0206da3ad..d195a07dc 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerLandscape_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2bb85f6de24c908d3df82607db3b6d38c3364a081a7e2a9714b8550941d9d64 -size 3970148 +oid sha256:0399f104345fe4f0356e170609504f34d1a29ba7dd8dcb7335c015209a101819 +size 4032352 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png index d55ec2e2c..41dedf6fa 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait-Selected_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:118279d65ddf5d4a5b2908ccfe7936d031eab39e2610f73ac84428ab441f5924 -size 3139081 +oid sha256:97f20d2a12b608f2199c3add311b056c1207cf2fc870adab795aea9ac783d6b6 +size 3234158 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png index 0e77c92f2..888d05165 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d717e9eedc4a4222d55c435ab630c794ecdf1fe44a1d24e278234a89e6b4330c -size 3504333 +oid sha256:80f8ef6b3412b4c65ac6cd3a0df6929ae01079acd79d72e2f13bd8ab754fad40 +size 3525556 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_accessibility.png index 2d528a6b7..676e4ee9a 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b8e4b215550d5a0194cb79f56278c0e032e6b47db6721115f78ff865c303a59 -size 3417322 +oid sha256:67dacac08164dfba2b92a014e1fac355c735fe2b436d686566e0203cc78b1d94 +size 3572559 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png index 78715c780..22d23f382 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:844b5beda8ae6e7d0e23c5cfc85d94431d87e8a1f1e2e3f8e41575e6208a6be7 -size 3133899 +oid sha256:7ae9dc0f925201b2ae1c219387b25543ee0f524080f86ac69a15e0e18c3d159f +size 3123234 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png index f9263e932..31a4649b7 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop-Selected_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70a809fe61480e2d150a1d9215707f97fa97f13c35960ed6b3f215cf80a83529 -size 1953622 +oid sha256:2d87ff2ca38a907bc440b4c857b8fc72593694cbf72b88923cd1a166b142afe5 +size 2114502 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop_duoInnerPortrait.png index 6d4549592..c4c9487d2 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Locations-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a964726b036fbfd92bbcd626e62ebce71747aad2d3f1df6ca5846bc0973d4ffc -size 1975625 +oid sha256:73c211cfef4b9724996a07d637b4f3e1ded5bfd7f275b1134861f56fc617272d +size 2140306 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-book_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-book_duoInnerLandscape.png index 47ce184ee..2c1d0afd5 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-book_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-book_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37a536bb79570bc042b430741941d018e0e3a56691564b58a0edaf14d4339b6e -size 1094872 +oid sha256:74482b80ae5441e89a61ebda287b65436c66b72dc2da57c5956d5e2267e6d218 +size 308675 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerLandscape_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerLandscape_duoInnerLandscape.png index 47ce184ee..2c1d0afd5 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerLandscape_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerLandscape_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37a536bb79570bc042b430741941d018e0e3a56691564b58a0edaf14d4339b6e -size 1094872 +oid sha256:74482b80ae5441e89a61ebda287b65436c66b72dc2da57c5956d5e2267e6d218 +size 308675 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait-Selected_duoInnerPortrait.png index 5d35512cb..2ed02f8c3 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait-Selected_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait-Selected_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a379067f6cb0e049a498904ff0c15e9cda07e0cbffb71084e79dc4607fdcc36b -size 2325781 +oid sha256:31208ae28db42f55bc1f195ddc583d653aad54675207d5adc5c65d0037377038 +size 1103517 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait.png index 13c7ff0d8..a032f60ae 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41480f7c8d43a1865fa9f1b5e87a34b4db8377affc6314fb8c520f1585bd609d -size 1437913 +oid sha256:1dc4ea0021823b8af5520cef2c853142afc6f46fe03cec5e16040ebe12110e2a +size 344555 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_accessibility.png index 11df5a612..4d4bedc87 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:899a49354dcb16fc739756ded98095f40969ced163b01df7a17905e862c34ac4 -size 1647970 +oid sha256:547a43da5afc03fe52c63fbbdcdce47a190472458b0ff5288835cbc107c27439 +size 761059 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_dark.png index 42df66b0e..f17d66e27 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cb553eef4f4a1204b09ac5d30f8c93691cce65c654b491367640dde53657565 -size 949799 +oid sha256:9809a56d548b66c8b690dd9f93d3d784a470e408dae7b914ec5527ef666cd30b +size 1015025 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-outerLandscape_duoOuterLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-outerLandscape_duoOuterLandscape.png index 0bb1e59ab..2e9c7162e 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-outerLandscape_duoOuterLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-outerLandscape_duoOuterLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de453ab504dea99e6a5de9308b760b3e37db7094065cc3672d55654bae4e1662 -size 156762 +oid sha256:36db0a534d2d04f15e49c4305f429a3f26ff40163a52a98e6611209b2d1814b5 +size 155481 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-tabletop_duoInnerPortrait.png index 13c7ff0d8..a032f60ae 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Settings-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41480f7c8d43a1865fa9f1b5e87a34b4db8377affc6314fb8c520f1585bd609d -size 1437913 +oid sha256:1dc4ea0021823b8af5520cef2c853142afc6f46fe03cec5e16040ebe12110e2a +size 344555 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png index a0c654d19..f0b1647e0 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a04a9e9a7794ea9956b012ab1d61f6a7f8afbf1402b925ef2d3708c9a74c7793 -size 755178 +oid sha256:c5254057dd249e4534b9f98c906797348a7513dc2478200ccf3d4e5a049c58cb +size 723817 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png index 4da74317b..ea06aad42 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:add2032d21b9f861b07d17765cbfaaab6b4f756d8bfa18e70c3eed039414d219 -size 2614152 +oid sha256:e18a6ea5227bd10c01e847f678c154bb0f92227a516017fa50fd6d68e11ca494 +size 2654271 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png index f97852f3f..ea06aad42 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2610ca4762603726dcede826c389c36f89c11b58dcad416989876b88ef11788f -size 2614150 +oid sha256:e18a6ea5227bd10c01e847f678c154bb0f92227a516017fa50fd6d68e11ca494 +size 2654271 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png index 7e783f7ad..860b18a30 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03400ef739502acd2b54b401bbfbecf303c3e3e9b56d7ba3851301e8151e8bff -size 835702 +oid sha256:34620447e74c7d45651bcbfd312e504d1bf8ebe354fe5d5ec805982cda44295e +size 817661 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png index 55c5798b0..da220b290 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93853e4e1fcddc91942ec66af135ff4fe035f4f9f714c8379ebe2675f9c27ab2 -size 2289728 +oid sha256:c55729b7b5d0988e62c546cc5375d0961b54000e315df672a26e08eb18db8ea5 +size 2305985 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png index a6f6b916b..d4a8c7163 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0dc1a2bf1a03445874bb9f58531f86fe4c92c909e852d90a8b71ef21b1ada43 -size 3552085 +oid sha256:f5867f52f117f2bab39877e0e59decda7315de552386bb8fbc70a2a04dde51f1 +size 2986707 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png index c1fd1d273..e2e622310 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c31ff6f711d02e45c9cc2f82427f6940f1c04ef73d8b9728b483b089b3f75e33 -size 2708749 +oid sha256:320ef528f073749ecbf29ad093aad024f7557f3d523c77758246808b15fd3220 +size 2685842 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png index 3a43dd572..ffb33d73b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-outerLandscape_duoOuterLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1307784675a873167fe8b8a7a486d039eb110819e3f0bcd0427d7611f8651cc4 -size 1621240 +oid sha256:6ccbf93ebfe512bd93e4d497d950d50b86b966d171dd19a72880d409f08bf257 +size 1636022 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png index 9cba6d909..dc8f66a13 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79a3868f0d747fcef8b180961e687188a7a341ff0e5784cfdb3fd37026ca529f -size 684781 +oid sha256:16c8779617a288a6fd4ce47119438597d3ee0f4dce12435e42e09a2b59b51f4d +size 697521 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png index 0e483ba55..be2e4a9f7 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f93a0dd7ae3820b88e107c3b27198794769eb6b513441ce9aaab378c8f93f13 -size 476078 +oid sha256:0dc2463d9a4d50577abf4817348c175846923b9bc69185e2e016bc7b1680c2e1 +size 479665 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png index 4d118d330..8373c7495 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6964a1229a9838cae95eaf8624be934f9819f8fe4595dfdf91ebd58bbeac03d3 -size 615374 +oid sha256:0139e1064caefe1ea706584b039d293fae9c002590dbeb7b5c0c82ee65de3911 +size 605529 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.UpdateAvailable_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.UpdateAvailable_iPhone_accessibility.png index 0c55de518..412ef0a15 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.UpdateAvailable_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.UpdateAvailable_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59919eaf8812f40f7ddbe1be755d4fed2925c164d7369d296f64c16b22d3635d -size 387029 +oid sha256:985d486479cb507b259df867d939d07667604213c34b04935a157671b6a02cb8 +size 375333 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeActionRequired_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeActionRequired_iPhone_accessibility.png index 03bf55f38..51194a94d 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeActionRequired_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeActionRequired_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f2c75f9d9691694f1f96e884c643f39adb72348ca9616a4edf5e8121c6ec6b3 -size 422258 +oid sha256:cc802ca508bff4f86ec34850be990244cc006d2eca46a69f5cdf1613c2142b04 +size 398366 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocating_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocating_iPhone_accessibility.png index faaae2697..676eaaf67 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocating_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocating_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf0323f162f5f5982ebde1b406ecc5d53bcae2cd4397d74543c03aeb5e5fdc35 -size 419032 +oid sha256:fdd59124389083711b4c32b4055fa544407cc7edc06a02a3780fc5a0aefbc532 +size 393480 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png index 10fb8cea4..fd0aafaf6 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8d3333e4170a24dfb059e6318ec5dbd82500e5bc7961957f9ce2c45b0d06fcc -size 4435476 +oid sha256:7b6c00ae3b9f3fdc19324ad1545ad47de0b3236067394d8cf15029920e864f2b +size 4704095 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png index f59d0aa9e..a15fc5e41 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:316cdaa178c4eced3c5b38bef0cacede78324d70f9f47d85088047fe007cc7cc -size 3471495 +oid sha256:db8a654b860e1135c9500b3e9e859b78f20fd7cb4630cc7ea89066527428b8fc +size 3471453 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png index 01353bdcd..b477e6f9c 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Locations-innerPortrait_duoInnerPortrait_rtl.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ee5c8eea91fe4bea0b45c524deb26741eb9fc7c73f249f9aa55da209a8a3b75 -size 3339070 +oid sha256:b44e434f36aaac5c03d787dceeee1d3c8a32d80aac951539b68b5cf5898aecc5 +size 3278655 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png index 1018d4b32..5f8769165 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2fb615a8077b3b509d88d813eefdd68eb594ed112542c3b3c301c56deaf2ec9 -size 319163 +oid sha256:4bbf69b85d1e34f56a33c3a3396975def13ea23e80e6c48fa76ee8dc67d9de61 +size 528324 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png index 5a0e5d170..d4949db2f 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9152d8cfc7fd2e463235bad2f279ff946c5f5a0c526075318ff9dc0678cb471 -size 1330322 +oid sha256:aa02a69b6f52f86018f05b6cd4cea7a8a18e3b6c9df1c05aa223a93ad5ef4bb6 +size 335855 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png index 433c6b738..fd10ef1f2 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Settings-innerPortrait_duoInnerPortrait_rtl.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b672469cbc433cd04793c1139fed6dbb15d137499ea4145729f772d5991e27a -size 1181073 +oid sha256:0b584a172288a2e92b0ca3633acb7d56b54443c60814ad9feaba3bcb18e59d1f +size 347013 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png index f76a390b3..f40168279 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b9691b355011ac2dae317e807c9877c7794f8848267b4146746bb7df65b4b56 -size 3647843 +oid sha256:ee252bdcaf761913ea18a31df54d1282a9c85ca7d643d688047bc452ee4a53dd +size 3740858 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png index 8edc835e6..01154b891 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1872aeb51172821bc722b7f011c7a15e42729138bbe3741b87728d443e377540 -size 2328517 +oid sha256:cbf5b037edfb4bcc7ead999cacd50661f78f442d475345d8af043db0ed5c35b1 +size 2341397 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png index 5700cf7e3..6d7a6bb45 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3235cd87b6a3c9ffb8c022f887fe64935f28641a66c837a377daaa2d1e7d4899 -size 2297555 +oid sha256:0de9ca45024be2aeea6b351dc0b9fb2e55f2ceee7acb6305dad1779a0cb1e345 +size 2318890 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.Flights_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.Flights_iPad_contrast.png index b34bbcb63..be6230552 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.Flights_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.Flights_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d950d5a24b606cdde550402b5429a022db65baa54a9db1d5d321d0fed01f4e36 -size 3050937 +oid sha256:6ea05c75b3a6496f395ab767ed5bd9c5951d0470e98cdc9427bc754d3576e435 +size 3050930 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.Flights_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.Flights_iPhone_contrast.png index a1cf95e16..fb60b7c82 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.Flights_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.Flights_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1aa4dbab16800f27a6dfc2ecd21a7064cd888228c0f8bbbb024338cd4c3896fa -size 1714447 +oid sha256:8eb3f60198d091a53e2e69899010f3b4f2135c89ce9bf0c059f48d6948d24328 +size 1714459 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.WithData_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.WithData_iPad_contrast.png index 39b883d9e..61879056b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.WithData_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.WithData_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f8029fe31d39ec7ce70ce58a3a8daa0248eddbdb6206b56f3cf8dbeb201cacd -size 2922689 +oid sha256:de3839262b0a6970850f0c517e8d32db9a0a82c59bd5e7313f28c3cebb7185bc +size 2922772 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.WithData_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.WithData_iPhone_contrast.png index 8a0d0c9b8..813b68ed6 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.WithData_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/other/presenceTimeline.WithData_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98e38d1c592e6bc81f6d91f3a03e0fb3ac7054abdb6fe4518836a7d7bc074b15 -size 1610886 +oid sha256:e8a0c287ec439af9a7c0867a4b3c472e5cbeb90e7cf82f60b4966945db75cf5a +size 1610873 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.BrowseColumn_browseColumn.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.BrowseColumn_browseColumn.png index cbc3f7324..05949d7d3 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.BrowseColumn_browseColumn.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.BrowseColumn_browseColumn.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d3d67dfb58120ade321f60414851ff40f7e114afc1f14a5f23af366f6bf3f52 -size 236358 +oid sha256:c5b163541a3f3256235b843245989b64bac123aa3ff36e841e046aa87d6a82d3 +size 236048 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad.png index e30975536..e3d6f2659 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bfdf458053294ca4f26b36cc1402e492c7886f14581fe86518c78863b54a032 -size 3027404 +oid sha256:119e739eda0dc465bf4ac58de2750d51f498d3878a2a83dd5e31a86b54e53de1 +size 3027408 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad_accessibility.png index 36919e8c1..69a4d051f 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eabd492a7a8fb91a847108e55d19e6c687ffe5567d00bff1b3979305991d3541 -size 3318082 +oid sha256:c8a02611fc35561b036cfb5ec9f00d4fe2017705868d276c809892565edc79aa +size 3329038 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad_dark.png index 81cecc7ba..f32ede18d 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4b7f097056e57410341ed4520637ca43a7325087172b7c453044e922dc7a278 -size 1792927 +oid sha256:33b76e85ec575be97e3697f0ff98441375787079066a23a6614c41303f3b095b +size 1792910 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone.png index 487bf88ae..f492af261 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16d35380ba1174758a6941cbc1ebdb954825fc04cb4fe47f7083a610512bd268 +oid sha256:d29230d1bab14583ce0bb3ca80a03960e691cb7855849bcf3a7a01392a3a4fe0 size 1702919 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone_accessibility.png index 442431a5e..f4f0e5968 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e8e0e0d8a1478b2f9da1b85be7ea6a1c151a9f2c9e4cfcc789f094f11008f2d -size 2027515 +oid sha256:48f058f9857915608f3d39dc6d8530b3b418732602ecd8e0331217731a65e44f +size 2038737 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone_dark.png index 6bcf159c2..60df465c3 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9057d8a5d345012119ea71cb62824ec6ccb1ca7e8432fd64189886fff067cd7 -size 1032034 +oid sha256:46dc7f946b356c85e5df964f8e01044ad94f1f989a483a68318c8597ef99e7cd +size 1032025 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InitialBottom_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InitialBottom_iPhone.png index b2f4e3659..c3e389226 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InitialBottom_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InitialBottom_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9c794f1096bb5d93696e6c01e192b3da8a1680c4ca5166f91237e65559b0dd8 -size 1495404 +oid sha256:9e3289d5f1d3eb8a93ce8b384c894c1693b7357c17d2fe50750217fae9912e7d +size 1455084 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InitialBottom_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InitialBottom_iPhone_dark.png index 5f775af64..7e1c2a8f6 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InitialBottom_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InitialBottom_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01bea07555c94c7fee2c3ef5d01fdc62dd29bb554bef0b411e4fb219ff928a69 -size 824692 +oid sha256:987a752f7be2c495daca25c392489481df85b0365b450d1e01d17195dfc06626 +size 778534 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InterruptedStay_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InterruptedStay_iPhone.png index 277dc5874..8feb2105b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InterruptedStay_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InterruptedStay_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e99f7095c5f2565bb36c97dd99f624e9b88f73ed33c7a1d35494e21eaf9d55ea -size 1483859 +oid sha256:f46d247be96047afa8bb160e07f483b630a4afb3cc50f2f9198f95e9d036775d +size 1483877 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InterruptedStay_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InterruptedStay_iPhone_dark.png index 566f5cf6c..e9e5ddbd8 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InterruptedStay_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.InterruptedStay_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84816e98ac349e6177a3acb61bbe39f5db0b990972bde81169a53e0c3c6a6ded -size 837667 +oid sha256:bce0344484811686bc1e587e1d579c75f3b14993b215b7ca6b423125fce9a7fd +size 837675 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.PlannedStayAfterRecordingGap_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.PlannedStayAfterRecordingGap_iPhone.png index 0f4aecc3c..7e8a4acc3 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.PlannedStayAfterRecordingGap_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.PlannedStayAfterRecordingGap_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5a142c1056b2e26fcd1f7335cc8e3cd51a812f930df65704668f2782572a6fe -size 1576984 +oid sha256:c8f29a82ebb2a96d01cbdd04bf7cfc4eeaef2b08166116b6aa6d45d188ece607 +size 1576982 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.PlannedStayAfterRecordingGap_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.PlannedStayAfterRecordingGap_iPhone_dark.png index ef865fecf..abf690dba 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.PlannedStayAfterRecordingGap_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.PlannedStayAfterRecordingGap_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d3e48678b1578e2533f845086a303a04b173abee4be423228341f2ad4d3d7ff -size 873758 +oid sha256:9eb9d1cd52030f7365bb260eb9d9fc1d090032dc9632131a0c3694455d4d1986 +size 873756 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.UncertainArrival_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.UncertainArrival_iPhone_accessibility.png index b81ca4155..97b1cc31e 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.UncertainArrival_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.UncertainArrival_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:350bdba3097af42f1a953dae3b1e444ea8dea35b309913b9ec9237498dd6ae09 -size 1973767 +oid sha256:fcf630a11325bcbffdc6476055075216603c353a7bc333a34b87fadf0d584e8f +size 1983786 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad.png index 4f609a091..1e6322c8b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16c793c2766be9b78fac3965ebe870d554c980d3f30727c4892cf90a154f6cc5 -size 2901823 +oid sha256:c9b63a09207d095938f8658c7c9ee7ba3c7b51d1dfe2487ccc982f0d72372612 +size 2901853 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad_accessibility.png index 06dc96e5b..40e227670 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a6c71f95192699a82f315d0e6357842269397118362f1bae7f4023484f0f3c0 -size 3009692 +oid sha256:f831d6e25691d9d995772987842d58a4060928c1187dc101522da4b33c91e926 +size 3019263 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad_dark.png index 3b8284e85..610ae959c 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e0284b69392009cc74d9935741a94f4a1311a01b22908845e64c1bed2b47c1e -size 1658798 +oid sha256:39da7ec2ae4c92c05b93a51a3db4f6ff39977408e29d45e0618ef19826cd0e87 +size 1658951 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone.png index dce8624d2..60fbf1f4b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67d170e9d53450ba0732a797d78474827f7200c9c9818a237ce469f388748dc3 -size 1597879 +oid sha256:356ada7d6580af9760bc644eb129dd374838fe0262c457830d2e039e2f2ed6f2 +size 1597890 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone_dark.png index 2919cd0e6..c73bbbf46 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:980dbf7b0d8c0958d84532d251720053eacf9e2c369cda390098693110e90ac4 -size 922326 +oid sha256:513784303e7e6a82d7a0c0c0c05580c7d115b08d10189a16cde335c547175a2c +size 922354 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPad_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPad_ax5.png index ed90c61ef..a88a42ed6 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPad_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPad_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bd590e0a06c4b5f7736f2489c97607710b7a40c1802f8dd8f0407ead919fb39 -size 963567 +oid sha256:23696cf62d615196a71dea5ff9e6c4456d58fb5a6fabae5f03923e265486c1bd +size 990569 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPad_contrast.png index 5b6bd1dcb..dd5f3d8b3 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfce2458b9e8c49f427096b873e481719d0a391d92339034f1b30111071a9a37 -size 528211 +oid sha256:0697e9e047571b806ed0a72111fe576f611e00539e657377146ef5047dd7a994 +size 527376 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPhone_ax5.png index 31e3a3c4f..fd191bef9 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPhone_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPhone_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09b5807781503d5af954df40f95ab06c0a45a7c856f024fa62eb143913e0c037 -size 1031761 +oid sha256:2acda7a9eb46a0aafd2eb115583a408064130ca9bc0a22eca6433d35505b922e +size 1032560 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPhone_contrast.png index 2ad1ee04c..fbd5dc56c 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dfd903d562d0eae0355071bae3245168f71cbc9eadd4dd7da27e3681757968e -size 421981 +oid sha256:2d44aa731e1ee52f8a1dde0865860d6fb6b7fb284029e6609fa04b02ab76a461 +size 409131 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPhone_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPhone_rtl.png index 315f37a6b..1a6e3529e 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPhone_rtl.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/other/settings.Default_iPhone_rtl.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3420350b480da92c9b6540650cbe992b10cddd8300c91054f5b395290c4af1f3 -size 418966 +oid sha256:3769b2edef4ade5389c040dd6e23139c7bbd1eb77de264de974d5297bddf93e1 +size 401388 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.BrowseColumn_browseColumn.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.BrowseColumn_browseColumn.png index 33fac6ad8..2cadba2e4 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.BrowseColumn_browseColumn.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.BrowseColumn_browseColumn.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:559497878d67e4a1dd8da32a8293dc5ef93ffeace04be5d9266491230c90536e -size 401212 +oid sha256:65ed3ae599f1f7a6078fc4636a70f13946860876644e29377916dbce516c1f59 +size 385281 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPad.png index 2a2b43e6a..4cd25e700 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:581eef6897996d6d16b50ae05bba536cc999f24c7cc85f3a6e8d2659a859e4ca -size 522711 +oid sha256:a6f0c6f5e7abfcabb49a248555e62055630e81db876708b2c2c5c2c50b01ae47 +size 519682 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPad_accessibility.png index 2deb37985..49ebb5f33 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8af0ca7c044e15042bb9e27505091066dcef7538accfb2d34c424b80bc49fcf -size 876101 +oid sha256:e5727d3ba2326bb44cea8f8b77f770f34ee83680c3050100b746208b382ff6a4 +size 900953 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPad_dark.png index c6c6513df..5555040a9 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPad_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPad_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ed4b39b7bd9d313346848bec18b9c34aeec511a836d1b62fd694e5982233fba -size 546878 +oid sha256:063f3a2dd6b11469f0a9f103ad53b2d01d21068fffb4cbd86fd76ee1e1779260 +size 550394 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPhone.png index 515bb2608..765fe97c2 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bd99b69e5bf9defac6493e19a6faa39a03abfbf0f37a3f3a128bb19ba423718 -size 414697 +oid sha256:6689bca2aecc1d227a4eb30bfbdedaa14d033c7142cd00feb8ee116d4c590ba4 +size 399591 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPhone_accessibility.png index 7d250229d..1ffe24952 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f2eace564f8093f3d9ddbedd7bb04ebb61f609acafb6101bbdf9d08457935be -size 792144 +oid sha256:e013a674b9788b8414912fde19accfe20500f1b03ee6798ad8e4a238eae5def7 +size 780743 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPhone_dark.png index 81b8a21dd..ae3cf5ab6 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.Default_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4938a113a28c767aa698a50750033675dcf50ae95c014aff738a01bdde45b7ff -size 437503 +oid sha256:f97dac4bc74fafc4f7c5e2bbde284078d3a98c0b8fcb0f830d0bd0cf66d20126 +size 430752 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.DemoMode_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.DemoMode_iPhone.png index ad3873c26..201b9bcf8 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.DemoMode_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.DemoMode_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13ee37e1123c6e1c2aa7f9d9c0632f9f85253ff65167993fb28f6ecf55d12d65 -size 366253 +oid sha256:4c820ec0190b16825eb636a603b989d4a5b1ef44c466513a5d2ea10f1e81b926 +size 352643 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.DemoMode_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.DemoMode_iPhone_dark.png index 385661682..054a52fc5 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.DemoMode_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.DemoMode_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ad63c374042a7d0dd26f187b135ca512071ee44a5e2547e0f07e0acae399783 -size 376925 +oid sha256:918f51681114b7214a416aa83afcfd169fbb9aac991d13f485956045013d17ec +size 369238 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.LocalBuild_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.LocalBuild_iPhone.png index f992f7320..67e89a55e 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.LocalBuild_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.LocalBuild_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d5efd46a7820a80fad778b61fb770e2550ffb1e201a41ede46df0d3a3839a6c -size 437383 +oid sha256:98b5c2a4b2a172a89195352a849f014d1510b4e03818204b01457f3740d60697 +size 420266 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.LocalBuild_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.LocalBuild_iPhone_dark.png index 4747beb56..279c3066c 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.LocalBuild_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.LocalBuild_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8566e3de3f5a8863af41e895f41dfe024d3cd2d1db882864b56940b58c4f4f06 -size 461359 +oid sha256:f75ef9161aebee763f75f822ab2deed15bfb7fbdd92f75994fe927c1ab5bf2dc +size 454089 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.RecordingConfigurationWarning_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.RecordingConfigurationWarning_iPhone.png index 805a84612..2c5c19d68 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.RecordingConfigurationWarning_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.RecordingConfigurationWarning_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cb874334f833302309e631b2ae6bc5d12f415131e598ea9e084ab48e7b13cba -size 493754 +oid sha256:19aff267cbf12c3447dd390017296b6988dfe1779cc98286b7b0aac6ac0d8d57 +size 478938 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.RecordingConfigurationWarning_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.RecordingConfigurationWarning_iPhone_dark.png index 935d29820..1e5fc856b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.RecordingConfigurationWarning_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.RecordingConfigurationWarning_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db322126514fe42e4e67860ae0fb35ea7aa69db31dd6d2588b0d2e5e24a4dfe6 -size 521633 +oid sha256:bcdf0d7307107be7dd1616b75f8d4fecf5a28414116b57179c791549613bbfb6 +size 517649 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.UpdateAvailable_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.UpdateAvailable_iPhone.png index f39d28baa..838112acd 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.UpdateAvailable_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.UpdateAvailable_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4c9455de51eea89a5b0e9f96f0c918c4c66958a5d55f5a0e598309ecab9cdfa -size 443009 +oid sha256:2b3380c0f75fedb3f7d31d293443937569dd8ec2c5fb5df6bb5cbe9a4c78b7ec +size 427076 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.UpdateAvailable_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.UpdateAvailable_iPhone_dark.png index 50564248a..a74d39b75 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.UpdateAvailable_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SettingsViewSnapshotTests/settings.UpdateAvailable_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5935a989d3dba0cef0684f94a37e6ddd28240da4b4eeee2fba7b4ec3e86a2d1b -size 467100 +oid sha256:a3cef6c7b7763c74b3f4930187f5eaa07a78662c35ab2e49307b45290d7ddcdc +size 461420 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png index d8ac6d78d..71013be54 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2570ac71042d9aa3b8ef8e05f0659633770e710d22b8d9b70743eb77545e4bd9 -size 1452037 +oid sha256:e413d2f09dd061a638c32c128b6e6d968c76cc61d5afb04ec294afb6c0fb925a +size 539879 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png index e07b7c8bd..a092e86e0 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2685660d64f97858ce15e3f0db7dc0b47de9131daadf60fc5334461ae99483dc -size 344498 +oid sha256:f0583df4a7fc8402412a9a1fce12457aedac26145e13800bc22ff64292240b78 +size 334639 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png index 1af55b092..2fb83cd04 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e3fc62601804f0311e8b7d1fd8839b889b1044752bb56ecaba160ebb9d57b5f -size 1268756 +oid sha256:ea003378d649d766c20ce38cd62ed89e8c136e10596835f67b22966ca31805b4 +size 195988 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png index 6607a0bca..d3afd4c89 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99bd289479460605d15ae281fbca9dbd2920dabd40e7f06318f4e6bb57854075 -size 290708 +oid sha256:b9901b410c688acab07e9c37ccfbb29faeced49468ff03d7751c89389e272975 +size 290721 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png index 5838866e8..952a8561d 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ca46cf79b975262e048c1d02bee2e4ae3487479ed9e21ea46dc1251833aff30 -size 1551314 +oid sha256:f6f6109cc2dd79a2707e8b51fea06d1532971f09bf4cfb7228bfe04b28bbde43 +size 369791 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png index 4d4fa5be8..3b44c3a66 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f2a58fd04e81b9c440edb7fe169733c52dfb52cd66c49ec025056c08eb3d82c -size 569812 +oid sha256:1c23c394dadf9a859b8fdd45aff32cb08778e7f5436d8b579c8874400b55af2d +size 569290 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_accessibility.png index 13d475bba..cbf850e59 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:175851eff1c337217cb336ea5c8e264bcc0720cf1143f275dc80d6f14bcaa6d8 -size 4171506 +oid sha256:95fc4519d24263a9a43ef1ec4d748e95734638d20ed0a8a458c9451e29fc834f +size 4180789 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_accessibility.png index 55d0e6351..4ccfa2da9 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16e6782477bdf112e6f4a0af8fe02a519f55335f3c1af3edc5349826218153c1 -size 2714048 +oid sha256:cd97d9edca4821104e158a96232a9c0efcc9b001ed87b814efb3850cf32782d4 +size 2723534 diff --git a/Where/WhereUI/Sources/Settings/SettingsContent.swift b/Where/WhereUI/Sources/Settings/SettingsContent.swift index 0acacfb49..e5bb26d61 100644 --- a/Where/WhereUI/Sources/Settings/SettingsContent.swift +++ b/Where/WhereUI/Sources/Settings/SettingsContent.swift @@ -94,6 +94,7 @@ struct SettingsContent: View { .navigationTitle(String(localized: .settingsTitle)) .searchable( text: $navigation.searchText, + placement: .navigationBarDrawer(displayMode: .always), prompt: String(localized: .settingsSearchPrompt), ) .overlay { diff --git a/Where/WhereUI/Tests/MainTabsTests.swift b/Where/WhereUI/Tests/MainTabsTests.swift index c6c0dbbfc..1057b35d7 100644 --- a/Where/WhereUI/Tests/MainTabsTests.swift +++ b/Where/WhereUI/Tests/MainTabsTests.swift @@ -75,6 +75,48 @@ struct MainTabsTests { @MainActor struct MainTabsTestsDuo { + @Test func settingsSearchStaysVisibleAcrossFoldLayouts() async throws { + let world = try await WhereFlyoverWorld.build() + let navigation = SceneNavigationModel() + navigation.selectedTab = .settings + let state = AdaptiveHistoryHostState() + let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + MainTabs(world: world, navigation: navigation) + .environment(world.model) + .environment(world.session) + .environment(\.isInDemoMode, true) + }) + host.safeAreaRegions = [] + try await show(host) { controller in + for pose in [ + AdaptiveHistoryPose.outerPortrait, + .innerPortrait, + .tabletop, + .book, + .outerPortrait, + ] { + try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) + let window = try #require(controller.view.window) + try await waitForAsync { + AdaptiveHistoryTestSupport.descendants( + of: window, + matching: UISearchTextField.self, + ) + .contains { field in + var ancestor: UIView? = field + while let view = ancestor { + if view.isHidden || view.alpha == 0 { return false } + ancestor = view.superview + } + let frame = field.convert(field.bounds, to: controller.view) + return frame.width > 0 && frame.height > 0 && controller.view.bounds + .contains(frame) + } + } + } + } + } + @Test func foldingKeepsOnePresentedEditorAndItsUnsavedDate() async throws { let world = try await WhereFlyoverWorld.build() let navigation = SceneNavigationModel() From f2bf635b91f46844db5e9d24c457748acdf0cbbd Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Wed, 11 Nov 2026 17:27:11 -0500 Subject: [PATCH 10/18] Keep Duo presentation and restoration in stable view owners --- Where/WhereUI/AGENTS.md | 8 +- Where/WhereUI/README.md | 40 +- .../LocationsViewSnapshotTests.swift | 2 +- .../SnapshotTests/YearViewSnapshotTests.swift | 2 +- .../DuoLayout/AdaptiveHistoryLabModel.swift | 2 - .../DuoLayout/AdaptiveHistoryLabView.swift | 30 +- .../AdaptiveHistorySnapshotFixture.swift | 37 +- Where/WhereUI/Sources/MainTabs.swift | 58 +-- .../WhereUI/Sources/Model/CalendarModel.swift | 91 +++++ .../Model/CalendarPresentationModel.swift | 161 -------- .../Sources/Primary/CalendarContentView.swift | 320 ++++++++------- .../Primary/CalendarPresentationState.swift | 57 +++ .../Sources/Primary/CalendarZoomOverlay.swift | 24 +- .../CalendarZoomTransitionState.swift} | 22 +- .../Primary/HistoryMonthControls.swift | 30 +- .../Sources/Primary/LocationsCardsView.swift | 43 +- .../Sources/Primary/LocationsView.swift | 85 +++- .../Primary/PresenceTimelineList.swift | 122 +++--- .../Sources/Settings/SettingsContent.swift | 6 +- .../Sources/Settings/SettingsView.swift | 31 +- Where/WhereUI/Sources/Year/YearView.swift | 98 +++-- .../Tests/CalendarContentViewTests.swift | 382 +++++++++++++----- Where/WhereUI/Tests/CalendarModelTests.swift | 46 +++ .../CalendarPresentationModelTests.swift | 247 ----------- .../CalendarPresentationStateTests.swift | 20 + ...=> CalendarZoomTransitionStateTests.swift} | 77 ++-- Where/WhereUI/Tests/LocationsViewTests.swift | 126 +++++- Where/WhereUI/Tests/MainTabsTests.swift | 39 +- .../Tests/PlannedStayEditorTests.swift | 3 +- .../Tests/PresenceTimelineListTests.swift | 104 ++++- .../Tests/ScrollPositionTestSupport.swift | 29 ++ .../Support/CalendarPresentationFixture.swift | 45 +++ .../Tests/Support/HostedAccessibility.swift | 31 ++ 33 files changed, 1481 insertions(+), 937 deletions(-) create mode 100644 Where/WhereUI/Sources/Model/CalendarModel.swift delete mode 100644 Where/WhereUI/Sources/Model/CalendarPresentationModel.swift create mode 100644 Where/WhereUI/Sources/Primary/CalendarPresentationState.swift rename Where/WhereUI/Sources/{Model/CalendarZoomTransitionModel.swift => Primary/CalendarZoomTransitionState.swift} (92%) create mode 100644 Where/WhereUI/Tests/CalendarModelTests.swift delete mode 100644 Where/WhereUI/Tests/CalendarPresentationModelTests.swift create mode 100644 Where/WhereUI/Tests/CalendarPresentationStateTests.swift rename Where/WhereUI/Tests/{CalendarZoomTransitionModelTests.swift => CalendarZoomTransitionStateTests.swift} (83%) create mode 100644 Where/WhereUI/Tests/ScrollPositionTestSupport.swift create mode 100644 Where/WhereUI/Tests/Support/CalendarPresentationFixture.swift create mode 100644 Where/WhereUI/Tests/Support/HostedAccessibility.swift diff --git a/Where/WhereUI/AGENTS.md b/Where/WhereUI/AGENTS.md index 780765c6e..d06601b56 100644 --- a/Where/WhereUI/AGENTS.md +++ b/Where/WhereUI/AGENTS.md @@ -114,9 +114,9 @@ Layering, localization, preview, and testing conventions live in the feature `YearReportModel` scan result. Inject that scene as the Resolve model's `ResolutionSource`. Re-key on raw evidence changes; never gate refresh on aggregate-report equality. Schedule presentation deadlines only in the foreground. - Expose each calendar month as one accessibility element: regional totals in the label, chronological stay spans and dated badges in the value. Preserve planning qualifiers. Guard: `CalendarMonthAccessibilityTests`. -- Keep overview availability derived from the full-size calendar visibility rule in `CalendarPresentationModel`. Unavailable months keep their region and projection colors, and remain actionable for rejection feedback. Guard: `CalendarPresentationModelTests`. -- Inject the calendar zoom key at each entry point. Persist mode changes through `WherePreferences`; a nil key must not read or write preferences. Guard: `CalendarPresentationModelTests`. -- Animate calendar zoom in an overlay after the destination scroll position settles. Keep moving geometry outside the scroll-target layout and cancel it when its viewport or report changes. Reduce Motion uses a crossfade. Guards: `CalendarZoomTransitionModelTests`, `CalendarContentViewTests`. +- Keep overview availability derived from the full-size calendar visibility rule in `CalendarModel`. Unavailable months keep their region and projection colors, and remain actionable for rejection feedback. Guard: `CalendarModelTests`. +- Inject the calendar zoom key at each entry point. Persist mode changes through `CalendarModel` and the existing `WherePreferences` keys; a nil key must not read or write preferences. Guard: `CalendarModelTests`. +- Animate calendar zoom in an overlay after the destination scroll position settles. Keep moving geometry outside the scroll-target layout and cancel it when its viewport or report changes. Reduce Motion uses a crossfade. Guards: `CalendarZoomTransitionStateTests`, `CalendarContentViewTests`. - Keep calendar flight days and timeline flight events independent of review dismissal. Show uncertain arrival timing explicitly. Render supported travel periods separately from later ground observations. Split timeline stays only for presentation; preserve every region-day exactly once. @@ -163,6 +163,8 @@ Layering, localization, preview, and testing conventions live in the feature - Keep navigation paths, search, sheets, and drafts in stable owning views above adaptive branches. Keep active drill-in stacks mounted while folding; defer tabletop composition until returning. - Read system reserved regions only in `AdaptivePaneGeometry`. Keep authored pane limits in the stylesheet and measured geometry in the adapter. Guard: `where.reserved_region_layout_ownership`. - Keep explicit reveal requests in the owning view, with typed request identities distinct from content identities. Use BroadwayUI scroll restoration for current-context anchors; scrolling must not select or move another pane. +- Keep rendered calendar anchors typed as `CalendarPresentationState.Target`, preserving month and estimate destinations. Detail owns the retained native scroll position; overview and tabletop must not overwrite it without an explicit reveal. +- Keep reusable pane content outside native navigation shells. Present planning sheets above changing branches. Embedded demo tools use `demoExitPresentation = .embedded`. - Keep synthetic-region injection under `#if DEBUG` and expose it through the Testing SPI. Transition tests retain one mounted container. - Keep itinerary persistence, forecast bounds, overlaps, and Home projections in WhereCore. `LocationForecastModel` mirrors one planning snapshot through store-change refreshes. diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index b800537c0..9f85ecd43 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -38,7 +38,7 @@ The calendar initially opens at full size. It remembers the last zoom level sepa `CalendarContentView` accepts an optional `CalendarZoomKey`; `nil` keeps zoom in memory without reading or writing preferences. Pinch inward or tap Show Year to see all twelve months in a three-column overview. Tap an available month or pinch outward over it to return to full size. The full-size estimate panel sits below the overview grid. Tapping it opens its position in the detailed calendar. -Visible months move and scale between their measured positions in each zoom level. The estimate panel moves at full size. `CalendarZoomTransitionModel` holds the outgoing tiles until the destination scroll position settles; `CalendarZoomOverlay` animates fixed-layout copies without changing the scroll layout. Reduce Motion uses a crossfade. +Visible months move and scale between their measured positions in each zoom level. The estimate panel moves at full size. `CalendarZoomTransitionState` holds the outgoing tiles until the destination scroll position settles; `CalendarZoomOverlay` animates fixed-layout copies without changing the scroll layout. Reduce Motion uses a crossfade. Gray month titles and day numbers mark months unavailable in the full-size view. Their region and projection colors remain visible. Activating one gives a light haptic and a brief shake. The overview uses stronger region fills, with extra opacity in dark mode. Future plans retain diagonal stripes; possible dates retain hollow dots and dashed outlines. Month cards are transparent in both zoom levels, so the screen background shows through. The current month uses an accent outline and title. @@ -69,10 +69,19 @@ An automatic check that can reuse Core's cache preserves an active manual check Demo mode hides this surface and performs no update checks. -`MainTabs` owns one report model and one `SceneNavigationModel` for the session. -The navigation model retains tab selection, region routes, the compact year mode, Settings routes, search, and calendar anchors. -Each destination receives its own child state. Calendar, timeline, and Locations anchors survive pane remounts. -The calendar keeps its native scroll binding in the mounted view. Retained presentation state restores the typed month or estimate target after a remount. The initial bottom alignment survives remounts until browsing, selection, zoom, or a year change replaces it. +`MainTabs` owns the report and its tab selection. Locations, Year, and Settings own +presentation state above adaptive layout branches. Their paths, search, and sheets +remain local to those screens. Active Locations drill-ins keep their native stack +mounted and defer tabletop composition until the user returns. + +An explicit BroadwayUI restoration scope retains each pane's native `ScrollPosition`. +Entries use the view type and a typed role; region/year changes replace the current +entry. There is no historical cache, persistence, or pixel-offset restoration. +The detail calendar retains typed month and forecast targets with their alignment. +Overview has a separate local position. Tabletop reads the selected month or detail +anchor without replacing a saved forecast target. +`CalendarModel` interprets available months and accesses existing zoom preferences. +View-owned helpers handle mode, gestures, rejection feedback, and transition geometry. A report-year change waits for matching data before positioning. Stale visibility callbacks cannot replace the new year's anchor. Calendar sections and day rows measure eagerly so offscreen estimates cannot move a restored month. The timeline keeps its pinned overview outside the scrolling rows; large text places it in the content flow. @@ -93,7 +102,7 @@ An active horizontal division places the selected month above the hinge and brow A vertical division retains native browse/detail navigation. Native containers own system-bar placement. The adapter uses container geometry, without device-name or hinge-angle checks. -`HistoryNavigationModel` links explicit month and stay selections through typed day intervals. +`HistorySelectionModel` links explicit month and stay selections through typed day intervals. It highlights the interval and reveals it in the companion pane. Scroll observations only retain local anchors. Each month reveal has a typed request identity. Consuming it preserves that identity, so repeated selections work without an observer feedback loop. Planning sheets sit above the changing containers, with one shared presentation owner per destination. @@ -101,9 +110,9 @@ The developer overlay and welcome overlay use the same geometry adapter to find The DEBUG **Duo Layout Lab** retains one production demo world while its pose control changes bounds, traits, and synthetic regions. Embedded demo tools omit the session's Exit Demo action. The containing tool owns dismissal. -Its measurements come from the pinned runtime; see [Duo measurements](../../Shared/SnapshotKit/Documentation/DuoMeasurements.json). +Synthetic poses come from `SnapshotConfiguration.Pose`; see the measurement provenance in [SnapshotKit](../../Shared/SnapshotKit/README.md). Native runtime geometry validation remains pending. Viewport shell captures supplement full-content captures of the shared scrolling panes. -Shell fixtures request their initial browse position after asynchronous demo preparation. +Views apply their initial position once the corresponding content is ready. Synthetic layouts do not validate system bars, camera activation, or physical folding. Those require native-device checks. The reusable panes also support iPad layouts. There is no HomeView target. @@ -738,18 +747,3 @@ Developer Overlay snapshots retain their scene for native glass rendering. Detac windows produce material bands and clipping in the menu. The retained-scene cases keep their device traits, content clearance, and full-content sizing. -## Adaptive pane foundations - -Stable destination views own navigation and presentation state. BroadwayUI retains -scroll positions for the current context only. Changing region or year discards -the old position. Explicit reveal requests belong to their consuming views; -ordinary scrolling does not select or move another pane. Synthetic pose geometry -and localized labels come from `SnapshotConfiguration.DuoPose`. - -`AdaptivePaneGeometry` maps safe areas and reserved regions into physical -container coordinates. `AdaptivePaneArrangement` applies the stylesheet's pane -limits. `AdaptivePaneContainer` hosts compact, columns, and tabletop arrangements. -The DEBUG Testing SPI supplies synthetic reserved regions to the same adapter. - -This foundation does not replace the current destination containers. The dependent -app integration installs view-owned state and presents these arrangements. diff --git a/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift index a647d697d..a92f6b122 100644 --- a/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift @@ -11,7 +11,7 @@ struct LocationsViewSnapshotTests { @Test func adaptiveLoadStates() async throws { for state in AdaptiveHistoryLoadFixture.State.allCases { - for pose in [AdaptiveHistoryPose.innerPortrait, .tabletop] { + for pose in [SnapshotConfiguration.Pose.innerPortrait, .tabletop] { let fixture = try await AdaptiveHistoryLoadFixture(state: state) await assertSnapshots( of: LocationsView(report: fixture.report) diff --git a/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift index da105ae5a..4edd176ed 100644 --- a/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift @@ -11,7 +11,7 @@ struct YearViewSnapshotTests { @Test func adaptiveLoadStates() async throws { for state in AdaptiveHistoryLoadFixture.State.allCases { - for pose in [AdaptiveHistoryPose.innerPortrait, .tabletop] { + for pose in [SnapshotConfiguration.Pose.innerPortrait, .tabletop] { let fixture = try await AdaptiveHistoryLoadFixture(state: state) await assertSnapshots( of: YearView(report: fixture.report) diff --git a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabModel.swift b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabModel.swift index d40c89382..de74837ff 100644 --- a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabModel.swift +++ b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabModel.swift @@ -11,8 +11,6 @@ case failed(Error) } - var pose = AdaptiveHistoryPose.outerPortrait - let navigation = SceneNavigationModel() private(set) var state = State.idle private(set) var request = 0 diff --git a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift index 25564d12b..9273df541 100644 --- a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift +++ b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift @@ -1,24 +1,32 @@ #if DEBUG + import RegionKit import SnapshotKit import SwiftUI /// A DEBUG-only mounted production scene. Pose controls change geometry, never its models. struct AdaptiveHistoryLabView: View { @State private var model: AdaptiveHistoryLabModel + @State private var pose: SnapshotConfiguration.Pose + private let initialRegion: Region? - init(model: AdaptiveHistoryLabModel = AdaptiveHistoryLabModel()) { + init( + model: AdaptiveHistoryLabModel = AdaptiveHistoryLabModel(), + pose: SnapshotConfiguration.Pose = .outerPortrait, + initialRegion: Region? = nil, + ) { _model = State(initialValue: model) + _pose = State(initialValue: pose) + self.initialRegion = initialRegion } @Environment(\.stylesheet) private var stylesheet var body: some View { - @Bindable var model = model VStack(spacing: stylesheet.spacing.medium) { Text(String(localized: .developerDuoTitle)) .font(.headline) - Picker(String(localized: .developerDuoPose), selection: $model.pose) { - ForEach(AdaptiveHistoryPose.allCases) { pose in + Picker(String(localized: .developerDuoPose), selection: $pose) { + ForEach(SnapshotConfiguration.Pose.allCases) { pose in Text(pose.title).tag(pose) } } @@ -55,13 +63,12 @@ private func preview(world: WhereFlyoverWorld) -> some View { GeometryReader { proxy in - let pose = model.pose let insets = pose.frame.safeAreaInsets let scale = min( 1, min(proxy.size.width / pose.size.width, proxy.size.height / pose.size.height), ) - MainTabs(world: world, navigation: model.navigation) + MainTabs(world: world, region: initialRegion) .environment(world.model) .environment(world.session) .environment(\.isInDemoMode, true) @@ -87,9 +94,8 @@ extension AdaptiveHistoryLabView: SnapshotProviding { static var snapshots: [SnapshotCase] { - [AdaptiveHistoryPose.outerPortrait, .tabletop].map { pose in + [SnapshotConfiguration.Pose.outerPortrait, .tabletop].map { pose in let fixture = AdaptiveHistorySnapshotFixture(tab: .locations, selection: .region) - fixture.model.pose = pose return whereSnapshot( name: pose.rawValue, configurations: [SnapshotConfiguration(device: .viewport( @@ -104,8 +110,12 @@ ))], onReadyToSnapshot: { await fixture.prepare() }, ) { - AdaptiveHistoryLabView(model: fixture.model) - .environment(\.regionOutlinePathCache, fixture.artwork) + AdaptiveHistoryLabView( + model: fixture.model, + pose: pose, + initialRegion: .california, + ) + .environment(\.regionOutlinePathCache, fixture.artwork) } } } diff --git a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistorySnapshotFixture.swift b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistorySnapshotFixture.swift index 6a6e0bb47..6f824343a 100644 --- a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistorySnapshotFixture.swift +++ b/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistorySnapshotFixture.swift @@ -37,11 +37,13 @@ let model = AdaptiveHistoryLabModel() let artwork = RegionOutlinePathCache() - private let selection: Selection + let tab: MainTabSelection.TabID + let history = HistorySelectionModel() + let selection: Selection private var didSelect = false - init(tab: SceneNavigationModel.TabID, selection: Selection) { - model.navigation.selectedTab = tab + init(tab: MainTabSelection.TabID, selection: Selection) { + self.tab = tab self.selection = selection } @@ -57,7 +59,7 @@ switch selection { case .none: break case .region: - model.navigation.locations.select(region: .california) + break case .stay: if let report = world.report.report, let stint = PresenceTimeline.stints( @@ -65,18 +67,18 @@ calendar: world.report.calendar, ).first { - model.navigation.year.history.select( + history.select( stint: stint, dateCalendar: world.report.calendar, ) } case let .month(month): - model.navigation.year.history.select( + history.select( month: month, dateCalendar: world.report.calendar, ) case .settings: - model.navigation.settings.selectedRoute = SettingsRoute(.appearance) + break } } for region in LocationsBackgroundArtwork.regions(in: world.report.ranking) { @@ -98,7 +100,7 @@ struct AdaptiveHistorySnapshotScene: View { let fixture: AdaptiveHistorySnapshotFixture - let pose: AdaptiveHistoryPose + let pose: SnapshotConfiguration.Pose var body: some View { Group { @@ -106,12 +108,19 @@ case .idle, .loading: ProgressView(String(localized: .primaryLoading)) case let .loaded(world): - MainTabs(world: world, navigation: fixture.model.navigation) - .environment(world.model) - .environment(world.session) - .environment(\.isInDemoMode, true) - .environment(\.demoExitPresentation, .embedded) - .environment(\.regionOutlinePathCache, fixture.artwork) + MainTabs( + world: world, + tab: fixture.tab, + region: fixture.selection == .region ? .california : nil, + settingsRoute: fixture + .selection == .settings ? SettingsRoute(.appearance) : nil, + history: fixture.history, + ) + .environment(world.model) + .environment(world.session) + .environment(\.isInDemoMode, true) + .environment(\.demoExitPresentation, .embedded) + .environment(\.regionOutlinePathCache, fixture.artwork) case let .failed(error): ContentUnavailableView( String(localized: .commonLoadErrorTitle), diff --git a/Where/WhereUI/Sources/MainTabs.swift b/Where/WhereUI/Sources/MainTabs.swift index 9d2392087..6d182bc87 100644 --- a/Where/WhereUI/Sources/MainTabs.swift +++ b/Where/WhereUI/Sources/MainTabs.swift @@ -1,6 +1,7 @@ #if DEBUG @_spi(Testing) import InstallerKitUI #endif +import BroadwayUI import InstallerKit import RegionKit import SFSafeSymbols @@ -21,7 +22,7 @@ import SwiftUI /// checked wiring); the always-on `WhereSession` coordinator stays in the /// environment. struct MainTabs: View { - private typealias TabID = SceneNavigationModel.TabID + typealias TabID = MainTabSelection.TabID private struct WelcomeTaskID: Hashable { let isActive: Bool @@ -32,7 +33,10 @@ struct MainTabs: View { @State private var recordingWarning: RecordingConfigurationWarningModel @State private var welcome: LocationWelcomeModel @State private var planningDestination: PlannedStaysDestination? - @State private var navigation = SceneNavigationModel() + @State private var selection = MainTabSelection() + private var initialRegion: Region? + private var initialSettingsRoute: SettingsRoute? + private var initialHistory: HistorySelectionModel? @Environment(UpdateModel.self) private var updates: UpdateModel? @Environment(\.isInDemoMode) private var isInDemoMode @Environment(\.scenePhase) private var scenePhase @@ -65,8 +69,7 @@ struct MainTabs: View { } var body: some View { - @Bindable var navigation = navigation - TabView(selection: $navigation.selectedTab) { + TabView(selection: $selection.selected) { Tab( String(localized: .tabLocations), systemSymbol: .locationFill, @@ -74,8 +77,8 @@ struct MainTabs: View { ) { LocationsView( report: report, - navigation: navigation.locations, - openElsewhere: { navigation.selectedTab = .elsewhere }, + initialRegion: initialRegion, + openElsewhere: { selection.selected = .elsewhere }, ) .reportingDeveloperTabBarInset() } @@ -86,13 +89,13 @@ struct MainTabs: View { systemSymbol: .globeAmericas, value: TabID.elsewhere, ) { - ElsewhereView(report: report, isSelected: navigation.selectedTab == .elsewhere) + ElsewhereView(report: report, isSelected: selection.selected == .elsewhere) .reportingDeveloperTabBarInset() } } Tab(String(localized: .tabYear), systemSymbol: .calendar, value: TabID.year) { - YearView(report: report, navigation: navigation.year) + YearView(report: report, history: initialHistory) .reportingDeveloperTabBarInset() } @@ -100,7 +103,7 @@ struct MainTabs: View { SettingsView( report: report, recordingWarning: recordingWarning, - navigation: navigation.settings, + initialRoute: initialSettingsRoute, ) .reportingDeveloperTabBarInset() } label: { @@ -113,9 +116,10 @@ struct MainTabs: View { .badge((recordingWarning.isPresented ? 1 : 0) + ((!isInDemoMode && updates?.needsAttention == true) ? 1 : 0)) } + .bScrollRestorationScope() .accessibilityHidden(welcomePresentation != nil) .onChange(of: report.hasElsewhere, initial: true) { _, hasElsewhere in - navigation.reconcile(hasElsewhere: hasElsewhere) + selection.reconcile(hasElsewhere: hasElsewhere) } .tabViewBottomAccessory(isEnabled: welcomeAccessory != nil) { if let welcomeAccessory { @@ -214,11 +218,17 @@ struct MainTabs: View { preferences: recordingWarningSource.preferences, )) _welcome = State(initialValue: welcome) - _navigation = State(initialValue: SceneNavigationModel(selectedTab: selection)) + _selection = State(initialValue: MainTabSelection(selected: selection)) } /// Retains the production shell around a separately built in-memory lab world. - init(world: WhereFlyoverWorld, navigation: SceneNavigationModel) { + init( + world: WhereFlyoverWorld, + tab: TabID = .locations, + region: Region? = nil, + settingsRoute: SettingsRoute? = nil, + history: HistorySelectionModel? = nil, + ) { self.init( session: world.session, report: world.report, @@ -227,9 +237,11 @@ struct MainTabs: View { preferences: world.scope.preferences, now: world.session.now, ), - selection: navigation.selectedTab, + selection: tab, ) - _navigation = State(initialValue: navigation) + initialRegion = region + initialSettingsRoute = settingsRoute + initialHistory = history } #endif } @@ -337,9 +349,9 @@ struct MainTabs: View { /// Viewport composition supplements the full-content Locations, calendar, timeline, /// and Settings matrices. Synthetic regions exercise the production layout policy. private static var adaptiveSnapshots: [SnapshotCase] { - let tabs: [SceneNavigationModel.TabID] = [.locations, .year, .settings] + let tabs: [MainTabSelection.TabID] = [.locations, .year, .settings] var cases = tabs.flatMap { tab in - AdaptiveHistoryPose.allCases.map { pose in + SnapshotConfiguration.Pose.allCases.map { pose in adaptiveSnapshot( tab: tab, pose: pose, @@ -349,7 +361,7 @@ struct MainTabs: View { } } for tab in tabs { - let pose = AdaptiveHistoryPose.innerPortrait + let pose = SnapshotConfiguration.Pose.innerPortrait let configurations = [ SnapshotConfiguration(colorScheme: .dark, device: pose.frame), SnapshotConfiguration(dynamicType: .accessibility5, device: pose.frame), @@ -366,7 +378,7 @@ struct MainTabs: View { ) } } - for pose in [AdaptiveHistoryPose.innerPortrait, .tabletop, .book] { + for pose in [SnapshotConfiguration.Pose.innerPortrait, .tabletop, .book] { cases.append(adaptiveSnapshot( tab: .locations, pose: pose, @@ -397,8 +409,8 @@ struct MainTabs: View { } private static func adaptiveSnapshot( - tab: SceneNavigationModel.TabID, - pose: AdaptiveHistoryPose, + tab: MainTabSelection.TabID, + pose: SnapshotConfiguration.Pose, configuration: SnapshotConfiguration, selection: AdaptiveHistorySnapshotFixture.Selection, ) -> SnapshotCase { @@ -422,11 +434,7 @@ struct MainTabs: View { settle: .settledAtLeast(minDuration: 1.5), onReadyToSnapshot: { await fixture.prepare() - // Request the fixture's starting position after demo preparation; - // native navigation insets can change while the shell appears. - if tab == .locations { - fixture.model.navigation.locations.scrollPosition.scrollTo(edge: .top) - } + }, ) { AdaptiveHistorySnapshotScene(fixture: fixture, pose: pose) diff --git a/Where/WhereUI/Sources/Model/CalendarModel.swift b/Where/WhereUI/Sources/Model/CalendarModel.swift new file mode 100644 index 000000000..3aed17fcc --- /dev/null +++ b/Where/WhereUI/Sources/Model/CalendarModel.swift @@ -0,0 +1,91 @@ +import Foundation +import RegionKit +import WhereCore + +/// Interprets calendar data and reads/writes the existing per-calendar zoom preference. +@MainActor +struct CalendarModel { + let report: YearReportModel + let persistenceKey: CalendarZoomKey? + + func isOverview(fallback: Bool) -> Bool { + persistenceKey.flatMap { report.preferences.calendarOverview(forKey: $0) } ?? fallback + } + + func setOverview(_ value: Bool) { + guard let persistenceKey else { return } + report.preferences.setCalendarOverview(value, forKey: persistenceKey) + } + + /// Inputs that invalidate a cached month grid. + struct LoadID: Equatable { + let report: YearReport + let missingDayKeys: Set + let evidenceDayKeys: Set + let flightDayKeys: Set + let referenceDay: Date + let focusedRegion: Region? + } + + func loadID(for yearReport: YearReport, focusedRegion: Region?) -> LoadID { + LoadID( + report: yearReport, + missingDayKeys: report.missingDayKeys, + evidenceDayKeys: report.evidenceDayKeys, + flightDayKeys: report.flightDayKeys, + referenceDay: report.calendar.startOfDay(for: report.referenceDate), + focusedRegion: focusedRegion, + ) + } + + func months( + from yearReport: YearReport, + focusedRegion: Region?, + ) -> Result<[CalendarMonth], Error> { + Result { + try yearReport.calendarMonths( + calendar: report.calendar, + referenceDate: report.referenceDate, + missingDates: report.missingDayKeys, + evidenceDays: report.evidenceDayKeys, + flightDays: report.flightDayKeys, + focusedRegion: focusedRegion, + ) + } + } + + /// The months to show in chronological order. Future months are omitted + /// unless a planned stay reaches into them; a past year shows the full year. + static func visibleMonths( + _ months: [CalendarMonth], + report: YearReportModel, + focusedRegion: Region?, + ) -> [CalendarMonth] { + guard + let currentMonthStart = report.calendar + .dateInterval(of: .month, for: report.referenceDate)? + .start + else { + return months + } + let lastPlannedDay = report.showsEstimatedTimeAndPlanning + ? report.forecasts.plannedIntervals(intersecting: report.selectedYear) + .filter { focusedRegion == nil || $0.region == focusedRegion } + .map(\.end).max() + : nil + let showsHome = report.showsEstimatedTimeAndPlanning + && report.forecasts.planning.homeRegion != nil + && (focusedRegion == nil || report.forecasts.planning.homeRegion == focusedRegion) + let finalDay = showsHome ? CalendarDay.lastDay(ofYear: report.selectedYear) : lastPlannedDay + if finalDay == nil, + report.selectedYear > report.calendar.component(.year, from: report.referenceDate) + { + // The year selector retains this selection after plans are hidden or deleted. + return months + } + let lastShownMonth = finalDay.map { + CalendarDay(year: $0.year, month: $0.month, day: 1).startOfDay(in: report.calendar) + }.map { max(currentMonthStart, $0) } ?? currentMonthStart + return months.filter { $0.startOfMonth <= lastShownMonth } + } +} diff --git a/Where/WhereUI/Sources/Model/CalendarPresentationModel.swift b/Where/WhereUI/Sources/Model/CalendarPresentationModel.swift deleted file mode 100644 index fb9614e1a..000000000 --- a/Where/WhereUI/Sources/Model/CalendarPresentationModel.swift +++ /dev/null @@ -1,161 +0,0 @@ -import Foundation -import Observation -import RegionKit -import WhereCore - -/// Owns calendar zoom intent and restores only destinations present in the detailed calendar. -@MainActor -@Observable -final class CalendarPresentationModel { - enum Mode: Hashable { - case detail, overview - } - - enum Target: Hashable { - case month(CalendarDay) - case estimate(CalendarDay) - - var month: CalendarDay { - switch self { - case let .month(month), let .estimate(month): month - } - } - } - - // Ratios relative to the start of one completed pinch. Small motions do not switch modes. - private static let overviewThreshold: CGFloat = 0.8 - private static let detailThreshold: CGFloat = 1.2 - - private(set) var mode: Mode { - didSet { - guard oldValue != mode, let persistenceKey else { return } - preferences.setCalendarOverview(mode == .overview, forKey: persistenceKey) - } - } - - private(set) var target: Target? - /// Preserve the first bottom-aligned placement until navigation or browsing replaces it. - private(set) var initialBottomTarget: Target? - private(set) var year: Int? - private(set) var availableMonths: Set = [] - private(set) var estimateMonth: CalendarDay? - private(set) var rejectedTarget: Target? - private(set) var rejectionCount = 0 - - private let preferences: WherePreferences - private let persistenceKey: CalendarZoomKey? - - init(mode: Mode, preferences: WherePreferences, persistenceKey: CalendarZoomKey?) { - self.preferences = preferences - self.persistenceKey = persistenceKey - self.mode = persistenceKey.flatMap { preferences.calendarOverview(forKey: $0) } - .map { $0 ? .overview : .detail } ?? mode - } - - func reconcile( - year: Int, - availableMonths: [CalendarDay], - currentMonth: CalendarDay?, - estimateMonth: CalendarDay?, - ) { - let changedYear = self.year != year - self.year = year - self.availableMonths = Set(availableMonths) - self.estimateMonth = estimateMonth - if changedYear || target.map({ !isAvailable($0) }) != false { - target = (currentMonth.flatMap { self.availableMonths.contains($0) ? $0 : nil } - ?? availableMonths.last).map(Target.month) - } - if changedYear || initialBottomTarget.map({ !isAvailable($0) }) == true { - initialBottomTarget = nil - } - if changedYear { rejectedTarget = nil } - } - - func isAvailable(_ target: Target) -> Bool { - switch target { - case let .month(month): availableMonths.contains(month) - case let .estimate(month): estimateMonth == month - } - } - - func prepareInitialPosition() { - initialBottomTarget = estimateMonth.map(Target.estimate) ?? target - } - - func remember(_ target: Target) { - guard mode == .detail, isAvailable(target) else { return } - initialBottomTarget = nil - self.target = target - } - - @discardableResult - func expand(_ destination: Target?) -> Bool { - guard let destination = destination ?? target else { return false } - guard isAvailable(destination) else { - rejectedTarget = destination - rejectionCount += 1 - return false - } - initialBottomTarget = nil - target = destination - mode = .detail - return true - } - - func toggle() { - switch mode { - case .detail: - initialBottomTarget = nil - mode = .overview - case .overview: expand(nil) - } - } - - func finishPinch(magnification: CGFloat, destination: Target?) { - switch mode { - case .detail: - if magnification <= Self.overviewThreshold { - initialBottomTarget = nil - mode = .overview - } - case .overview: - if magnification >= Self.detailThreshold { expand(destination) } - } - } - - /// The months to show in chronological order. Future months are omitted - /// unless a planned stay reaches into them; a past year shows the full year. - static func visibleMonths( - _ months: [CalendarMonth], - report: YearReportModel, - focusedRegion: Region?, - ) -> [CalendarMonth] { - guard - let currentMonthStart = report.calendar - .dateInterval(of: .month, for: report.referenceDate)? - .start - else { - return months - } - let lastPlannedDay = report.showsEstimatedTimeAndPlanning - ? report.forecasts.plannedIntervals(intersecting: report.selectedYear) - .filter { focusedRegion == nil || $0.region == focusedRegion } - .map(\.end).max() - : nil - let showsHome = report.showsEstimatedTimeAndPlanning - && report.forecasts.planning.homeRegion != nil - && (focusedRegion == nil || report.forecasts.planning.homeRegion == focusedRegion) - let finalDay = showsHome ? CalendarDay.lastDay(ofYear: report.selectedYear) : lastPlannedDay - if finalDay == nil, - report.selectedYear > report.calendar.component(.year, from: report.referenceDate) - { - // The year selector retains this selection after plans are hidden or deleted. - return months - } - let lastShownMonth = finalDay.map { - CalendarDay(year: $0.year, month: $0.month, day: 1).startOfDay(in: report.calendar) - }.map { max(currentMonthStart, $0) } ?? currentMonthStart - return months.filter { $0.startOfMonth <= lastShownMonth } - } -} diff --git a/Where/WhereUI/Sources/Primary/CalendarContentView.swift b/Where/WhereUI/Sources/Primary/CalendarContentView.swift index 66225c820..bdb353c18 100644 --- a/Where/WhereUI/Sources/Primary/CalendarContentView.swift +++ b/Where/WhereUI/Sources/Primary/CalendarContentView.swift @@ -1,3 +1,4 @@ +import BroadwayUI import PeriscopeCore import RegionKit import SFSafeSymbols @@ -11,11 +12,17 @@ import SwiftUI /// navigation: the Your Year tab embeds it inline, and the Locations tab pushes /// it — region-focused, with a title — as the zoom destination of a tapped card. struct CalendarContentView: View { - enum Presentation { case year, month } - let history: HistoryNavigationModel? + enum Presentation: Equatable { case year, month } + let history: HistorySelectionModel? let displayMode: Presentation - let ownsPlanningPresentation: Bool - @State private var navigation: CalendarNavigationModel + enum ScrollRole: Hashable { case standalone, locations, year } + struct ScrollContext: Hashable { let region: Region?; let year: Int } + private let model: CalendarModel + private let suppliedPlanningDestination: Binding? + @State private var localPlanningDestination: PlannedStaysDestination? + private var planningDestination: Binding { + suppliedPlanningDestination ?? $localPlanningDestination + } /// When set, the day grid only shows dots for this region (so it reads as /// "just the days I spent here"); the per-month footer still lists every @@ -26,49 +33,69 @@ struct CalendarContentView: View { @Environment(\.stylesheet) private var stylesheet @State private var monthsLoad: Result<[CalendarMonth], Error>? - @State private var presentation: CalendarPresentationModel - @AccessibilityFocusState private var focusedTarget: CalendarPresentationModel.Target? - @State private var zoom = CalendarZoomTransitionModel() - @State private var calendarScrollPosition = ScrollPosition(idType: CalendarPresentationModel - .Target.self) + @State private var localPresentation: CalendarPresentationState + private let suppliedPresentation: CalendarPresentationState? + private var presentation: CalendarPresentationState { + suppliedPresentation ?? localPresentation + } + + @AccessibilityFocusState private var focusedTarget: CalendarPresentationState.Target? + @State private var zoom = CalendarZoomTransitionState() + @BScrollPosition private var detailPosition: ScrollPosition + @State private var overviewPosition = ScrollPosition( + idType: CalendarPresentationState.Target.self, + edge: .top, + ) + @State private var tabletopPosition = ScrollPosition( + idType: CalendarPresentationState.Target.self, + edge: .top, + ) + private var detailTarget: CalendarPresentationState + .Target? + { + detailPosition.viewID(type: CalendarPresentationState.Target.self) + } + + private var calendarPosition: Binding { + if displayMode == .month { return $tabletopPosition } + return presentation.mode == .overview ? $overviewPosition : $detailPosition + } + @State private var scrollOffset = CGPoint.zero /// A nil persistence key keeps zoom session-only, including in previews. init( focusedRegion: Region? = nil, report: YearReportModel, - navigation: CalendarNavigationModel? = nil, - history: HistoryNavigationModel? = nil, + history: HistorySelectionModel? = nil, + presentationState: CalendarPresentationState? = nil, presentation: Presentation = .year, - initialMode: CalendarPresentationModel.Mode = .detail, + initialMode: CalendarPresentationState.Mode = .detail, persistenceKey: CalendarZoomKey? = nil, + scrollRole: ScrollRole = .standalone, + planningDestination: Binding? = nil, ) { self.focusedRegion = focusedRegion self.report = report self.history = history + suppliedPresentation = presentationState displayMode = presentation - ownsPlanningPresentation = navigation == nil && history == nil - let owner = history?.calendar ?? navigation ?? CalendarNavigationModel() - _navigation = State(initialValue: owner) - _presentation = State(initialValue: owner.presentation( - report: report, - initialMode: initialMode, - persistenceKey: persistenceKey, - )) + suppliedPlanningDestination = planningDestination + let model = CalendarModel(report: report, persistenceKey: persistenceKey) + self.model = model + _localPresentation = + State(initialValue: CalendarPresentationState(mode: model + .isOverview(fallback: initialMode == .overview) ? .overview : .detail)) + _detailPosition = BScrollPosition( + Self.self, + role: scrollRole, + context: { ScrollContext(region: focusedRegion, year: report.selectedYear) }, + idType: CalendarPresentationState.Target.self, + ) } private static let logger = WhereLog.session(CalendarViewLog.self) - /// Inputs that invalidate a cached month grid. - private struct CalendarLoadID: Equatable { - let report: YearReport - let missingDayKeys: Set - let evidenceDayKeys: Set - let flightDayKeys: Set - let referenceDay: Date - let focusedRegion: Region? - } - var body: some View { Group { if let yearReport = report.report { @@ -82,9 +109,9 @@ struct CalendarContentView: View { AppIconLoadingView(caption: String(localized: .primaryLoading)) } } - .task(id: calendarLoadID(report: yearReport)) { + .task(id: model.loadID(for: yearReport, focusedRegion: focusedRegion)) { zoom.cancel() - let result = loadCalendarMonths(from: yearReport) + let result = model.months(from: yearReport, focusedRegion: focusedRegion) guard !Task.isCancelled else { return } monthsLoad = result } @@ -123,15 +150,15 @@ struct CalendarContentView: View { .debugLogInspectable(WhereLog.session(CalendarViewLog.self)) .modifier(PlanningPresentation( report: report, - navigation: navigation.planning, - isEnabled: ownsPlanningPresentation, + destination: planningDestination, + isEnabled: suppliedPlanningDestination == nil, )) .toolbar(displayMode == .month ? .hidden : .automatic, for: .navigationBar) .toolbar { if displayMode == .year { ToolbarItem(placement: .topBarTrailing) { Button { - changeZoom { presentation.toggle() } + changeZoom { toggleZoom() } } label: { Label( presentation @@ -150,40 +177,17 @@ struct CalendarContentView: View { String(localized: .plannedStaysTitle), systemSymbol: .calendarBadgeClock, ) { - navigation.planningDestination = .list + planningDestination.wrappedValue = .list } } } } .onChange(of: report.selectedYear) { zoom.cancel() } + .onChange(of: presentation.mode) { model.setOverview(presentation.mode == .overview) } .onChange(of: stylesheet.calendar.overview.usesSpatialMotion) { zoom.cancel() } .onDisappear { zoom.cancel() } } - private func calendarLoadID(report yearReport: YearReport) -> CalendarLoadID { - CalendarLoadID( - report: yearReport, - missingDayKeys: report.missingDayKeys, - evidenceDayKeys: report.evidenceDayKeys, - flightDayKeys: report.flightDayKeys, - referenceDay: report.calendar.startOfDay(for: report.referenceDate), - focusedRegion: focusedRegion, - ) - } - - private func loadCalendarMonths(from yearReport: YearReport) -> Result<[CalendarMonth], Error> { - Result { - try yearReport.calendarMonths( - calendar: report.calendar, - referenceDate: report.referenceDate, - missingDates: report.missingDayKeys, - evidenceDays: report.evidenceDayKeys, - flightDays: report.flightDayKeys, - focusedRegion: focusedRegion, - ) - } - } - private func calendarLayoutError(_ error: Error) -> some View { ContentUnavailableView { Label(String(localized: .commonLoadErrorTitle), systemSymbol: .exclamationmarkIcloud) @@ -198,7 +202,7 @@ struct CalendarContentView: View { } private func calendarContent(months: [CalendarMonth]) -> some View { - let availableMonths = CalendarPresentationModel.visibleMonths( + let availableMonths = CalendarModel.visibleMonths( months, report: report, focusedRegion: focusedRegion, @@ -207,8 +211,8 @@ struct CalendarContentView: View { let estimateMonth = months.first(where: showsForecast(after:)) let visibleMonths = displayMode == .year ? availableMonths : months.filter { monthKey($0) == - (navigation.displayedMonth ?? currentMonth.map(monthKey) ?? availableMonths.last - .map(monthKey)) + (selectedMonth ?? detailTarget?.month ?? currentMonth + .map(monthKey) ?? availableMonths.last.map(monthKey)) } let overview = displayMode == .year && presentation.mode == .overview @@ -248,12 +252,12 @@ struct CalendarContentView: View { forecasts: calendarForecasts, microprintRegions: report.ranking.primary.map(\.region), homeRegion: report.forecasts.planning.homeRegion, - planningAction: { navigation.planningDestination = .list }, + planningAction: { planningDestination.wrappedValue = .list }, ) .anchorPreference(key: CalendarZoomFrames.self, value: .bounds) { [.detail: [.tile(.estimate(monthKey(month))): $0]] } - .id(CalendarPresentationModel.Target.estimate(monthKey(month))) + .id(CalendarPresentationState.Target.estimate(monthKey(month))) .accessibilityFocused( $focusedTarget, equals: .estimate(monthKey(month)), @@ -272,9 +276,10 @@ struct CalendarContentView: View { frames[overview ? .overview : .detail, default: [:]][.content] = content } } + // Each mode keeps its own binding while outgoing content finishes its transition. + .scrollPosition(calendarPosition) .id(presentation.mode) .transition(stylesheet.calendar.overview.transition) - .scrollPosition($calendarScrollPosition) .onScrollGeometryChange(for: CGPoint.self) { CGPoint( x: $0.contentOffset.x + $0.contentInsets.leading, @@ -283,13 +288,6 @@ struct CalendarContentView: View { } action: { _, value in scrollOffset = value } - .onScrollTargetVisibilityChange(idType: CalendarPresentationModel.Target.self) { targets in - if zoom.phase == nil, calendarScrollPosition.isPositionedByUser, - let first = targets.first - { presentation.remember(first) - navigation.recordVisibleMonths([first.month]) - } - } .simultaneousGesture(MagnifyGesture().onEnded { value in let viewportSize = zoom.measurement?.viewport.size ?? .zero let point = CGPoint( @@ -302,7 +300,7 @@ struct CalendarContentView: View { })?.key guard displayMode == .year else { return } changeZoom { - presentation.finishPinch( + finishPinch( magnification: value.magnification, destination: destination, ) @@ -316,68 +314,52 @@ struct CalendarContentView: View { .task(id: CalendarLayoutID( year: report.selectedYear, mode: presentation.mode, + displayMode: displayMode, availableMonths: availableMonths.map(monthKey), estimateMonth: estimateMonth.map(monthKey), )) { guard !Task.isCancelled else { return } - let isInitial = navigation.initiallyPositionedYear != report.selectedYear - navigation.restorePosition( - in: report.selectedYear, - initialMonth: (currentMonth ?? availableMonths.last).map(monthKey), - ) presentation.reconcile( year: report.selectedYear, - availableMonths: availableMonths.map(monthKey), - currentMonth: currentMonth.map(monthKey), + availableMonths: (displayMode == .month ? months : availableMonths).map(monthKey), estimateMonth: estimateMonth.map(monthKey), ) - if applyReveal() { return } - if displayMode == .year, !overview, - let target = presentation.target - { - if isInitial { presentation.prepareInitialPosition() } - let initialBottomTarget = presentation.initialBottomTarget - if zoom.phase == nil { - calendarScrollPosition.scrollTo( - id: initialBottomTarget ?? target, - anchor: initialBottomTarget == nil ? .top : .bottom, - ) - if initialBottomTarget == nil { focusedTarget = target } + let initialTarget = estimateMonth + .map { CalendarPresentationState.Target.estimate(monthKey($0)) } + ?? (currentMonth ?? availableMonths.last) + .map { CalendarPresentationState.Target.month(monthKey($0)) } + if let initialTarget { + _detailPosition.initialize(id: initialTarget, anchor: .bottom) + if let target = detailTarget, !presentation.isAvailable(target) { + _detailPosition.scrollTo(id: initialTarget, anchor: .bottom) } - } else if displayMode == .month, let month = navigation.displayedMonth, - month.year == report.selectedYear - { - calendarScrollPosition.scrollTo( - id: CalendarPresentationModel.Target.month(month), + } + if applyReveal() { return } + if displayMode == .year, presentation.mode == .detail, let target = detailTarget { + // A new native scroll view must receive the command after its content mounts. + _detailPosition.scrollTo(id: target, anchor: _detailPosition.anchor ?? .top) + } + if displayMode == .month, let month = selectedMonth ?? detailTarget?.month { + tabletopPosition.scrollTo( + id: CalendarPresentationState.Target.month(month), anchor: .top, ) - } else { - calendarScrollPosition.scrollTo(edge: .top) } } - .onChange(of: navigation.revealRequest, initial: true) { _, _ in - _ = applyReveal() - } + .onChange(of: history?.requestID, initial: true) { _ = applyReveal() } .overlayPreferenceValue(CalendarZoomFrames.self) { anchors in - GeometryReader { proxy in - let measured = CalendarZoomFrames.measure( - anchors[overview ? .overview : .detail] ?? [:], - in: proxy, - mode: overview ? .overview : .detail, - scrollOffset: scrollOffset, - ) - Color.clear - .onChange(of: measured, initial: true) { _, value in - zoom.updateMeasurement(value) - if case .preparing = zoom.phase, - let value, value.mode == .detail, - let target = presentation.target, value.frames[target] != nil - { - calendarScrollPosition.scrollTo(id: target, anchor: .top) - } - } - } - .allowsHitTesting(false) + Color.clear + .onGeometryChange(for: CalendarZoomTransitionState.Measurement?.self) { proxy in + CalendarZoomFrames.measure( + anchors[overview ? .overview : .detail] ?? [:], + in: proxy, + mode: overview ? .overview : .detail, + scrollOffset: scrollOffset, + ) + } action: { value in + zoom.updateMeasurement(value) + } + .allowsHitTesting(false) } .allowsHitTesting(zoom.phase == nil) .accessibilityHidden(zoom.phase != nil) @@ -387,7 +369,7 @@ struct CalendarContentView: View { zoomTile(target, mode: mode, months: months) }, completion: { id in guard zoom.finish(id) else { return } - if presentation.mode == .detail { focusedTarget = presentation.target } + if presentation.mode == .detail { focusedTarget = detailTarget } }) .background(stylesheet.calendar.overview.zoomBackground) } @@ -396,24 +378,58 @@ struct CalendarContentView: View { @discardableResult private func applyReveal() -> Bool { - guard let request = navigation.pendingReveal, + guard let history, let request = history.pendingRequest(for: .calendar), request.month.year == report.selectedYear, presentation.year == report.selectedYear, - displayMode == .month || presentation.isAvailable(.month(request.month)) + presentation.isAvailable(.month(request.month)) else { return false } zoom.cancel() - if displayMode == .year { presentation.expand(.month(request.month)) } - calendarScrollPosition.scrollTo( - id: CalendarPresentationModel.Target.month(request.month), - anchor: .top, - ) - navigation.consumeReveal(request) + let target = CalendarPresentationState.Target.month(request.month) + if displayMode == .year { _ = presentation.expand(target) } + _detailPosition.scrollTo(id: target, anchor: .top) + tabletopPosition.scrollTo(id: target, anchor: .top) + history.consume(request.id, by: .calendar) return true } + private var selectedMonth: CalendarDay? { + guard let day = history?.selectedInterval(in: report.selectedYear)?.lowerBound + else { return nil } + return CalendarDay(year: day.year, month: day.month, day: 1) + } + + private func expand(_ target: CalendarPresentationState.Target) { + guard presentation.expand(target) else { return } + _detailPosition.scrollTo(id: target, anchor: .top) + } + + private func toggleZoom() { + switch presentation.mode { + case .detail: presentation.mode = .overview + case .overview: + guard let target = detailTarget else { return } + _ = presentation.expand(target) + } + } + + private func finishPinch( + magnification: CGFloat, + destination: CalendarPresentationState.Target?, + ) { + switch presentation.mode { + case .detail: + if magnification <= CalendarPresentationState + .overviewThreshold { presentation.mode = .overview } + case .overview: + if magnification >= CalendarPresentationState.detailThreshold, + let destination { expand(destination) } + } + } + private struct CalendarLayoutID: Equatable { let year: Int - let mode: CalendarPresentationModel.Mode + let mode: CalendarPresentationState.Mode + let displayMode: Presentation let availableMonths: [CalendarDay] let estimateMonth: CalendarDay? } @@ -433,31 +449,27 @@ struct CalendarContentView: View { if stylesheet.calendar.overview.usesSpatialMotion, let source, source.mode == previousMode { - zoom.begin(from: source, to: presentation.mode, focus: presentation.target) + zoom.begin( + from: source, + to: presentation.mode, + focus: detailTarget, + alignment: _detailPosition.anchor ?? .top, + ) } zoom.updateMeasurement(nil) - switch presentation.mode { - case .overview: - calendarScrollPosition = ScrollPosition( - idType: CalendarPresentationModel.Target.self, - edge: .top, - ) - case .detail: - if zoom.phase != nil { - calendarScrollPosition = - ScrollPosition(idType: CalendarPresentationModel.Target - .self) - } else if let target = presentation.target { - calendarScrollPosition = ScrollPosition(id: target, anchor: .top) - } + if presentation.mode == .overview { + overviewPosition = ScrollPosition( + idType: CalendarPresentationState.Target.self, + edge: .top, + ) } } } @ViewBuilder private func zoomTile( - _ target: CalendarPresentationModel.Target, - mode: CalendarPresentationModel.Mode, + _ target: CalendarPresentationState.Target, + mode: CalendarPresentationState.Mode, months: [CalendarMonth], ) -> some View { switch target { @@ -474,7 +486,7 @@ struct CalendarContentView: View { forecasts: calendarForecasts, microprintRegions: report.ranking.primary.map(\.region), homeRegion: report.forecasts.planning.homeRegion, - planningAction: mode == .detail ? { navigation.planningDestination = .list } : + planningAction: mode == .detail ? { planningDestination.wrappedValue = .list } : nil, ) } @@ -484,7 +496,7 @@ struct CalendarContentView: View { CalendarDay(year: month.year, month: month.month, day: 1) } - private func target(for month: CalendarMonth) -> CalendarPresentationModel.Target { + private func target(for month: CalendarMonth) -> CalendarPresentationState.Target { .month(monthKey(month)) } @@ -512,7 +524,7 @@ struct CalendarContentView: View { private func overviewMonth(_ month: CalendarMonth, available: Bool) -> some View { let destination = target(for: month) - return Button { changeZoom { presentation.expand(destination) } } label: { + return Button { changeZoom { expand(destination) } } label: { monthView(month, compact: true, isAvailable: available) } .buttonStyle(.plain) @@ -528,8 +540,8 @@ struct CalendarContentView: View { } private func overviewEstimate(_ month: CalendarMonth) -> some View { - let destination = CalendarPresentationModel.Target.estimate(monthKey(month)) - return Button { changeZoom { presentation.expand(destination) } } label: { + let destination = CalendarPresentationState.Target.estimate(monthKey(month)) + return Button { changeZoom { expand(destination) } } label: { LocationForecastPanel( forecasts: calendarForecasts, microprintRegions: report.ranking.primary.map(\.region), diff --git a/Where/WhereUI/Sources/Primary/CalendarPresentationState.swift b/Where/WhereUI/Sources/Primary/CalendarPresentationState.swift new file mode 100644 index 000000000..05124a7a0 --- /dev/null +++ b/Where/WhereUI/Sources/Primary/CalendarPresentationState.swift @@ -0,0 +1,57 @@ +import Foundation +import Observation +import WhereCore + +/// View-owned zoom and rejection feedback. The native detail position owns restoration. +@MainActor @Observable +final class CalendarPresentationState { + enum Mode: Hashable { case detail, overview } + enum Target: Hashable { + case month(CalendarDay) + case estimate(CalendarDay) + var month: CalendarDay { + switch self { + case let .month(month), let .estimate(month): month + } + } + } + + // Completed pinch ratios relative to the start of the gesture. + static let overviewThreshold: CGFloat = 0.8 + static let detailThreshold: CGFloat = 1.2 + var mode: Mode + private(set) var year: Int? + private(set) var availableMonths: Set = [] + private(set) var estimateMonth: CalendarDay? + private(set) var rejectedTarget: Target? + private(set) var rejectionCount = 0 + + init(mode: Mode) { + self.mode = mode + } + + func reconcile(year: Int, availableMonths: [CalendarDay], estimateMonth: CalendarDay?) { + if self.year != year { rejectedTarget = nil } + self.year = year + self.availableMonths = Set(availableMonths) + self.estimateMonth = estimateMonth + } + + func isAvailable(_ target: Target) -> Bool { + switch target { + case let .month(month): availableMonths.contains(month) + case let .estimate(month): estimateMonth == month + } + } + + func expand(_ destination: Target?) -> Bool { + guard let destination else { return false } + guard isAvailable(destination) else { + rejectedTarget = destination + rejectionCount += 1 + return false + } + mode = .detail + return true + } +} diff --git a/Where/WhereUI/Sources/Primary/CalendarZoomOverlay.swift b/Where/WhereUI/Sources/Primary/CalendarZoomOverlay.swift index 13e835ca4..0affab29f 100644 --- a/Where/WhereUI/Sources/Primary/CalendarZoomOverlay.swift +++ b/Where/WhereUI/Sources/Primary/CalendarZoomOverlay.swift @@ -2,16 +2,16 @@ import SwiftUI /// Moves fixed-layout copies of the visible tiles while the real scroll view stays positioned. struct CalendarZoomOverlay: View { - let phase: CalendarZoomTransitionModel.Phase - let content: (CalendarPresentationModel.Target, CalendarPresentationModel.Mode) -> TileContent - let completion: (CalendarZoomTransitionModel.ID) -> Void + let phase: CalendarZoomTransitionState.Phase + let content: (CalendarPresentationState.Target, CalendarPresentationState.Mode) -> TileContent + let completion: (CalendarZoomTransitionState.ID) -> Void var body: some View { ZStack(alignment: .topLeading) { switch phase { case let .preparing(source): ForEach( - source.visibleFrames.keys.sorted(by: CalendarZoomTransitionModel.ordered), + source.visibleFrames.keys.sorted(by: CalendarZoomTransitionState.ordered), id: \.self, ) { target in if let frame = source.visibleFrames[target] { @@ -33,9 +33,9 @@ struct CalendarZoomOverlay: View { } private struct CalendarZoomFlight: View { - let plan: CalendarZoomTransitionModel.Plan - let content: (CalendarPresentationModel.Target, CalendarPresentationModel.Mode) -> TileContent - let completion: (CalendarZoomTransitionModel.ID) -> Void + let plan: CalendarZoomTransitionState.Plan + let content: (CalendarPresentationState.Target, CalendarPresentationState.Mode) -> TileContent + let completion: (CalendarZoomTransitionState.ID) -> Void @Environment(\.stylesheet) private var stylesheet @State private var arrived = false @@ -91,7 +91,7 @@ private struct CalendarZoomFlight: View { } } - private func cornerRadius(for target: CalendarPresentationModel.Target) -> CGFloat { + private func cornerRadius(for target: CalendarPresentationState.Target) -> CGFloat { switch target { case .month: let mode = arrived ? plan.source.destination : plan.source.measurement.mode @@ -102,7 +102,7 @@ private struct CalendarZoomFlight: View { } } - private func backgroundOpacity(for visibility: CalendarZoomTransitionModel.Tile.Visibility) + private func backgroundOpacity(for visibility: CalendarZoomTransitionState.Tile.Visibility) -> Double { switch visibility { @@ -114,8 +114,8 @@ private struct CalendarZoomFlight: View { } struct CalendarZoomFrames: PreferenceKey { - typealias Mode = CalendarPresentationModel.Mode - typealias Target = CalendarPresentationModel.Target + typealias Mode = CalendarPresentationState.Mode + typealias Target = CalendarPresentationState.Target enum Frame: Hashable { case content case tile(Target) @@ -128,7 +128,7 @@ struct CalendarZoomFrames: PreferenceKey { in proxy: GeometryProxy, mode: Mode, scrollOffset: CGPoint, - ) -> CalendarZoomTransitionModel.Measurement? { + ) -> CalendarZoomTransitionState.Measurement? { guard let content = anchors[.content] else { return nil } let contentFrame = proxy[content] // Resolve anchors relative to their content, then apply the current scroll diff --git a/Where/WhereUI/Sources/Model/CalendarZoomTransitionModel.swift b/Where/WhereUI/Sources/Primary/CalendarZoomTransitionState.swift similarity index 92% rename from Where/WhereUI/Sources/Model/CalendarZoomTransitionModel.swift rename to Where/WhereUI/Sources/Primary/CalendarZoomTransitionState.swift index cf64e07b6..eea1bf6d1 100644 --- a/Where/WhereUI/Sources/Model/CalendarZoomTransitionModel.swift +++ b/Where/WhereUI/Sources/Primary/CalendarZoomTransitionState.swift @@ -5,9 +5,9 @@ import SwiftUI /// Matching uses viewport rectangles, so moving tiles cannot alter scroll-target layout. @MainActor @Observable -final class CalendarZoomTransitionModel { - typealias Mode = CalendarPresentationModel.Mode - typealias Target = CalendarPresentationModel.Target +final class CalendarZoomTransitionState { + typealias Mode = CalendarPresentationState.Mode + typealias Target = CalendarPresentationState.Target /// Points: ignore subpixel rounding in viewport sizes and scroll-edge alignment. private nonisolated static let geometryTolerance: CGFloat = 1 @@ -26,6 +26,7 @@ final class CalendarZoomTransitionModel { let measurement: Measurement let destination: Mode let focus: Target? + let alignment: UnitPoint var visibleFrames: [Target: CGRect] { measurement.frames.filter { $0.value.intersects(measurement.viewport) } @@ -76,7 +77,12 @@ final class CalendarZoomTransitionModel { if let measurement { resolve(destination: measurement) } } - func begin(from measurement: Measurement, to destination: Mode, focus: Target?) { + func begin( + from measurement: Measurement, + to destination: Mode, + focus: Target?, + alignment: UnitPoint = .top, + ) { guard phase == nil, measurement.mode != destination, measurement.frames.values.contains(where: { $0.intersects(measurement.viewport) }) else { return } @@ -86,6 +92,7 @@ final class CalendarZoomTransitionModel { measurement: measurement, destination: destination, focus: focus, + alignment: alignment, )) } @@ -109,7 +116,12 @@ final class CalendarZoomTransitionModel { case .detail: guard let focus = source.focus, let frame = destination.frames[focus], frame.intersects(destination.viewport), - abs(frame.minY) < Self.geometryTolerance + abs(frame + .minY - + (destination.viewport + .minY + (destination.viewport.height - frame.height) * source + .alignment.y)) < + Self.geometryTolerance || destination.content.maxY <= destination.viewport.maxY + Self .geometryTolerance else { return } diff --git a/Where/WhereUI/Sources/Primary/HistoryMonthControls.swift b/Where/WhereUI/Sources/Primary/HistoryMonthControls.swift index d8b507336..58da171f3 100644 --- a/Where/WhereUI/Sources/Primary/HistoryMonthControls.swift +++ b/Where/WhereUI/Sources/Primary/HistoryMonthControls.swift @@ -1,3 +1,5 @@ +import BroadwayUI +import RegionKit import SFSafeSymbols import SwiftUI import WhereCore @@ -5,12 +7,32 @@ import WhereCore /// Explicit month browsing controls shared by both tabletop destinations. struct HistoryMonthControls: View { let report: YearReportModel - let history: HistoryNavigationModel + let history: HistorySelectionModel + @BScrollPosition private var position: ScrollPosition + init( + report: YearReportModel, + history: HistorySelectionModel, + region: Region? = nil, + scrollRole: CalendarContentView.ScrollRole = .year, + ) { + self.report = report + self.history = history + _position = BScrollPosition( + CalendarContentView.self, + role: scrollRole, + context: { CalendarContentView.ScrollContext(region: region, year: report.selectedYear) + }, + idType: CalendarPresentationState.Target.self, + ) + } + @Environment(\.stylesheet) private var stylesheet private var month: CalendarDay { - if let month = history.calendar.displayedMonth, month.year == report.selectedYear { - return month + if let day = history.selectedInterval(in: report.selectedYear)?.lowerBound ?? position + .viewID(type: CalendarPresentationState.Target.self)?.month + { + return CalendarDay(year: day.year, month: day.month, day: 1) } let today = CalendarDay(from: report.referenceDate, in: report.calendar) return CalendarDay( @@ -69,7 +91,7 @@ struct HistoryMonthControls: View { #Preview { HistoryMonthControls( report: PreviewSupport.loadedYearReportModel(), - history: HistoryNavigationModel(), + history: HistorySelectionModel(), ) .whereBroadwayRoot() } diff --git a/Where/WhereUI/Sources/Primary/LocationsCardsView.swift b/Where/WhereUI/Sources/Primary/LocationsCardsView.swift index 223067288..00c46dd2d 100644 --- a/Where/WhereUI/Sources/Primary/LocationsCardsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsCardsView.swift @@ -1,3 +1,4 @@ +import BroadwayUI import RegionKit import SwiftUI import WhereCore @@ -12,9 +13,40 @@ struct LocationsCardsView: View { let isCardSurfaceUncovered: Bool @Binding var isCardSurfaceVisible: Bool let planningAction: () -> Void - @Bindable var navigation: SceneNavigationModel.Locations + @Bindable var navigation: LocationsView.Navigation let cardVariant: WhereStylesheet.CardStyle.Variant + @BScrollPosition private var position: ScrollPosition + enum ScrollRole: Hashable { case browse } + + init( + report: YearReportModel, + cardPresentation: LocationCardsPresentationModel, + tilt: TiltProvider, + calendarTransition: Namespace.ID, + isCardSurfaceUncovered: Bool, + isCardSurfaceVisible: Binding, + planningAction: @escaping () -> Void, + navigation: LocationsView.Navigation, + cardVariant: WhereStylesheet.CardStyle.Variant, + ) { + self.report = report + self.cardPresentation = cardPresentation + self.tilt = tilt + self.calendarTransition = calendarTransition + self.isCardSurfaceUncovered = isCardSurfaceUncovered + _isCardSurfaceVisible = isCardSurfaceVisible + self.planningAction = planningAction + self.navigation = navigation + self.cardVariant = cardVariant + _position = BScrollPosition( + Self.self, + role: ScrollRole.browse, + context: { report.selectedYear }, + idType: Region.self, + ) + } + @Environment(\.stylesheet) private var stylesheet @Environment(\.regionStyles) private var regionStyles @@ -107,17 +139,16 @@ struct LocationsCardsView: View { } .defaultScrollAnchor(.top, for: .initialOffset) .defaultScrollAnchor(report.liveFlightReview == nil ? .center : .top, for: .alignment) - .scrollPosition($navigation.scrollPosition) - .onScrollTargetVisibilityChange(idType: Region.self, threshold: 0.01) { regions in - guard navigation.scrollPosition.isPositionedByUser else { return } - navigation.visibleRegion = presentedCards.first { regions.contains($0.region) }?.region + .scrollPosition($position) + .onChange(of: report.selectedYear, initial: true) { + _position.initialize(edge: .top) } .scrollBounceBehavior(.basedOnSize) // The card normally stays clipped to the scrolling viewport. Reveal // overflow only while the authored arc, scale, and rotation need it. .scrollClipDisabled(cardPresentation.isSpatialOvertakeActive) .accessibilityIdentifier("where_root_title") - .onAppear { isCardSurfaceVisible = true; navigation.restoreBrowsePosition() } + .onAppear { isCardSurfaceVisible = true } .onDisappear { isCardSurfaceVisible = false } // Count, order, flourish, persistence, and haptic all share this one // visibility-aware delayed reconciliation. diff --git a/Where/WhereUI/Sources/Primary/LocationsView.swift b/Where/WhereUI/Sources/Primary/LocationsView.swift index d532ae9fd..8288a236d 100644 --- a/Where/WhereUI/Sources/Primary/LocationsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsView.swift @@ -1,4 +1,5 @@ import BroadwayUI +import Observation import PeriscopeCore import RegionKit import SFSafeSymbols @@ -8,9 +9,38 @@ import WhereCore /// Stable destination ownership around native browse/detail and current-history tabletop layouts. struct LocationsView: View { + @MainActor @Observable + final class Navigation { + enum Destination: Hashable { + case flight(GPSCorrectionReview) + } + + var selectedRegion: Region? + var compactColumn = NavigationSplitViewColumn.sidebar + var browsePath = NavigationPath() + var detailPath = NavigationPath() + + var showingResolution = false + var planningDestination: PlannedStaysDestination? + + func select(region: Region) { + guard selectedRegion != region || compactColumn != .detail else { return } + if selectedRegion != region { detailPath = NavigationPath() } + selectedRegion = region + compactColumn = .detail + } + + func open(_ destination: Destination) { + browsePath.append(destination) + compactColumn = .sidebar + } + } + let report: YearReportModel var openElsewhere: (() -> Void)? - @State private var navigation: SceneNavigationModel.Locations + @State private var navigation: LocationsView.Navigation + @State private var history = HistorySelectionModel() + @State private var calendarPresentation: CalendarPresentationState @State private var cardPresentation: LocationCardsPresentationModel @State private var tilt = TiltProvider() @Namespace private var calendarTransition @@ -18,12 +48,19 @@ struct LocationsView: View { init( report: YearReportModel, - navigation: SceneNavigationModel.Locations? = nil, + navigation: LocationsView.Navigation? = nil, + initialRegion: Region? = nil, openElsewhere: (() -> Void)? = nil, ) { self.report = report self.openElsewhere = openElsewhere - _navigation = State(initialValue: navigation ?? SceneNavigationModel.Locations()) + let navigation = navigation ?? LocationsView.Navigation() + if let initialRegion { navigation.select(region: initialRegion) } + _navigation = State(initialValue: navigation) + _calendarPresentation = State(initialValue: CalendarPresentationState(mode: CalendarModel( + report: report, + persistenceKey: navigation.selectedRegion.map(CalendarZoomKey.region), + ).isOverview(fallback: false) ? .overview : .detail)) _cardPresentation = State(initialValue: LocationCardsPresentationModel( preferences: report.preferences, year: report.selectedYear, @@ -40,7 +77,12 @@ struct LocationsView: View { NavigationStack { browseContent .safeAreaInset(edge: .top, spacing: 0) { - HistoryMonthControls(report: report, history: navigation.history) + HistoryMonthControls( + report: report, + history: history, + region: navigation.selectedRegion, + scrollRole: .locations, + ) } } } @@ -80,7 +122,18 @@ struct LocationsView: View { .sheet(isPresented: $navigation.showingResolution) { ResolutionView(report: report) } - .modifier(PlanningPresentation(report: report, navigation: navigation.history.planning)) + .modifier(PlanningPresentation( + report: report, + destination: $navigation.planningDestination, + )) + .onChange(of: navigation.selectedRegion) { + history.reset() + calendarPresentation = CalendarPresentationState(mode: CalendarModel( + report: report, + persistenceKey: navigation.selectedRegion.map(CalendarZoomKey.region), + ).isOverview(fallback: false) ? .overview : .detail) + } + .onChange(of: report.selectedYear) { history.reset() } } private var browseContent: some View { @@ -99,7 +152,7 @@ struct LocationsView: View { private func tabletopPanes(for arrangement: AdaptiveHistoryArrangement) -> AdaptiveHistoryArrangement.Panes? { - guard navigation.browsePath.isEmpty, + guard navigation.browsePath.isEmpty, navigation.detailPath.isEmpty, case let .tabletop(panes) = arrangement else { return nil } return panes } @@ -113,9 +166,12 @@ struct LocationsView: View { CalendarContentView( focusedRegion: navigation.selectedRegion, report: report, - history: navigation.history, + history: history, + presentationState: calendarPresentation, presentation: presentation, persistenceKey: navigation.selectedRegion.map(CalendarZoomKey.region), + scrollRole: .locations, + planningDestination: $navigation.planningDestination, ) .id(navigation.selectedRegion) .navigationTitle(navigation.selectedRegion.map { @@ -148,7 +204,7 @@ private struct RegionCalendarTransition: ViewModifier { struct LocationsBrowsePane: View { let report: YearReportModel var openElsewhere: (() -> Void)? - @State private var navigation: SceneNavigationModel.Locations + @State private var navigation: LocationsView.Navigation @State private var cardPresentation: LocationCardsPresentationModel @State private var tilt: TiltProvider @Namespace private var localTransition @@ -157,7 +213,7 @@ struct LocationsBrowsePane: View { init( report: YearReportModel, - navigation: SceneNavigationModel.Locations? = nil, + navigation: LocationsView.Navigation? = nil, cardPresentation: LocationCardsPresentationModel? = nil, tilt: TiltProvider? = nil, calendarTransition: Namespace.ID? = nil, @@ -166,7 +222,7 @@ struct LocationsBrowsePane: View { ) { self.report = report self.openElsewhere = openElsewhere - _navigation = State(initialValue: navigation ?? SceneNavigationModel.Locations()) + _navigation = State(initialValue: navigation ?? LocationsView.Navigation()) _cardPresentation = State(initialValue: cardPresentation ?? LocationCardsPresentationModel( preferences: report.preferences, year: report.selectedYear, @@ -188,11 +244,10 @@ struct LocationsBrowsePane: View { cardVariant: cardVariant, openElsewhere: openElsewhere, ) - .navigationDestination(for: SceneNavigationModel.Locations.Destination + .navigationDestination(for: LocationsView.Navigation.Destination .self) { destination in switch destination { - case .elsewhere: ElsewhereView(report: report) case let .flight(review): FlightDayDetailView( review: review, report: report, @@ -212,7 +267,7 @@ struct LocationsContent: View { var openElsewhere: (() -> Void)? let cardVariant: WhereStylesheet.CardStyle.Variant - @State private var navigation: SceneNavigationModel.Locations + @State private var navigation: LocationsView.Navigation @State private var isCardSurfaceVisible = false @State private var cardPresentation: LocationCardsPresentationModel @@ -239,14 +294,14 @@ struct LocationsContent: View { init( report: YearReportModel, - navigation: SceneNavigationModel.Locations? = nil, + navigation: LocationsView.Navigation? = nil, cardPresentation: LocationCardsPresentationModel? = nil, tilt: TiltProvider? = nil, calendarTransition: Namespace.ID? = nil, cardVariant: WhereStylesheet.CardStyle.Variant = .regular, openElsewhere: (() -> Void)? = nil, ) { - _navigation = State(initialValue: navigation ?? SceneNavigationModel.Locations()) + _navigation = State(initialValue: navigation ?? LocationsView.Navigation()) self.report = report self.openElsewhere = openElsewhere self.cardVariant = cardVariant diff --git a/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift b/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift index b0e6ca59f..07e4a7140 100644 --- a/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift +++ b/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift @@ -1,3 +1,4 @@ +import BroadwayUI import RegionKit import SFSafeSymbols import SnapshotKit @@ -6,7 +7,6 @@ import WhereCore #if DEBUG import BroadwayCore - import BroadwayUI #endif /// A chronological list of continuous stays (`RegionStint`s) for the selected @@ -21,24 +21,36 @@ struct PresenceTimelineList: View { let report: YearReportModel let presentation: Presentation - private let ownsPlanningPresentation: Bool + private let suppliedPlanningDestination: Binding? + @State private var localPlanningDestination: PlannedStaysDestination? + @BScrollPosition private var position: ScrollPosition + enum ScrollRole: Hashable { case timeline } + private var planningDestination: Binding { + suppliedPlanningDestination ?? $localPlanningDestination + } @Environment(\.stylesheet) private var stylesheet - @State private var history: HistoryNavigationModel + @State private var history: HistorySelectionModel init( report: YearReportModel, presentation: Presentation = .full, - history: HistoryNavigationModel? = nil, + history: HistorySelectionModel? = nil, + planningDestination: Binding? = nil, ) { self.report = report self.presentation = presentation - ownsPlanningPresentation = history == nil - _history = State(initialValue: history ?? HistoryNavigationModel()) + suppliedPlanningDestination = planningDestination + _position = BScrollPosition( + Self.self, + role: ScrollRole.timeline, + context: { report.selectedYear }, + idType: HistorySelectionModel.Item.self, + ) + _history = State(initialValue: history ?? HistorySelectionModel()) } var body: some View { - @Bindable var navigation = history.timeline let yearReport = report.report let stints = yearReport .map { PresenceTimeline.stints(from: $0, calendar: report.calendar) } ?? [] @@ -55,7 +67,7 @@ struct PresenceTimelineList: View { today: report.forecasts.today, ) : [] - let rowIDs = recordedItems.map(TimelineNavigationModel.Item.init) + let rowIDs = recordedItems.map(HistorySelectionModel.Item.init) + plannedItems.map { .planned($0.id) } Group { @@ -129,7 +141,7 @@ struct PresenceTimelineList: View { forecasts: timelineForecasts, microprintRegions: report.ranking.primary.map(\.region), homeRegion: report.forecasts.planning.homeRegion, - planningAction: { history.planning.destination = .list }, + planningAction: { planningDestination.wrappedValue = .list }, ) } } @@ -137,49 +149,34 @@ struct PresenceTimelineList: View { .padding(.top, pinsOverview ? 0 : stylesheet.spacing.large) .padding(.bottom, stylesheet.spacing.large) } - .scrollPosition($navigation.scrollPosition) - .onScrollTargetVisibilityChange( - idType: TimelineNavigationModel.Item.self, - threshold: 0.01, - ) { visible in - guard navigation.scrollPosition.isPositionedByUser else { return } - navigation.recordVisibleItems( - rowIDs.filter { visible.contains($0) }, - in: report.selectedYear, - ) - } - .task(id: report.selectedYear) { - navigation.restorePosition( - in: report.selectedYear, - initialItem: presentation == .linked - ? recordedItems.last.map(TimelineNavigationModel.Item.init) : nil, - ) - navigation.reconcile( - recordedItems: recordedItems, - plannedItems: plannedItems, - calendar: report.calendar, - ) - } - .onChange(of: recordedItems) { - navigation.reconcile( - recordedItems: recordedItems, - plannedItems: plannedItems, - calendar: report.calendar, - ) - } - .onChange(of: plannedItems) { - navigation.reconcile( - recordedItems: recordedItems, - plannedItems: plannedItems, - calendar: report.calendar, - ) - } - .onChange(of: navigation.monthRevealID) { - navigation.reconcile( - recordedItems: recordedItems, - plannedItems: plannedItems, - calendar: report.calendar, - ) + .scrollPosition($position) + .task(id: TimelineLayoutID( + year: report.selectedYear, + recordedItems: recordedItems, + plannedItems: plannedItems, + request: history.requestID, + )) { + guard !Task.isCancelled else { return } + let initial = presentation == .linked ? recordedItems.last + .map(HistorySelectionModel.Item.init) : nil + if let initial { _position.initialize(id: initial, anchor: .top) } + else { _position.initialize(edge: .bottom) } + if let target = position.viewID(type: HistorySelectionModel.Item.self), + !rowIDs.contains(target) + { + if let initial { _position.scrollTo(id: initial, anchor: .top) } + else { _position.scrollTo(edge: .bottom) } + } + if let request = history.pendingRequest(for: .timeline), + let target = history.timelineTarget( + recordedItems: recordedItems, + plannedItems: plannedItems, + calendar: report.calendar, + ) + { + _position.scrollTo(id: target, anchor: .top) + history.consume(request.id, by: .timeline) + } } .defaultScrollAnchor( presentation == .linked ? .top : .bottom, @@ -191,8 +188,8 @@ struct PresenceTimelineList: View { } .modifier(PlanningPresentation( report: report, - navigation: history.planning, - isEnabled: ownsPlanningPresentation, + destination: planningDestination, + isEnabled: suppliedPlanningDestination == nil, )) .toolbar { if presentation != .excerpt, report.showsEstimatedTimeAndPlanning { @@ -201,13 +198,20 @@ struct PresenceTimelineList: View { String(localized: .plannedStaysTitle), systemSymbol: .calendarBadgeClock, ) { - history.planning.destination = .list + planningDestination.wrappedValue = .list } } } } } + private struct TimelineLayoutID: Equatable { + let year: Int + let recordedItems: [PresenceJourneyItem] + let plannedItems: [PlanningTimelineItem] + let request: HistorySelectionModel.Request.ID? + } + /// Both presentations share recorded and planned row rendering. @ViewBuilder private func journeyRows( @@ -256,7 +260,7 @@ struct PresenceTimelineList: View { } } .fixedSize(horizontal: false, vertical: presentation == .excerpt) - .id(TimelineNavigationModel.Item(item)) + .id(HistorySelectionModel.Item(item)) } ForEach(plannedItems) { item in let row = PlannedPresenceJourneyRow( @@ -290,7 +294,7 @@ struct PresenceTimelineList: View { edit(item) } } - .id(TimelineNavigationModel.Item.planned(item.id)) + .id(HistorySelectionModel.Item.planned(item.id)) } else { row } @@ -301,9 +305,9 @@ struct PresenceTimelineList: View { if case let .stay(interval) = item, let stay = report.forecasts.planning.stays.first(where: { $0.id == interval.stayID }) { - history.planning.destination = .edit(stay) + planningDestination.wrappedValue = .edit(stay) } else { - history.planning.destination = .list + planningDestination.wrappedValue = .list } } diff --git a/Where/WhereUI/Sources/Settings/SettingsContent.swift b/Where/WhereUI/Sources/Settings/SettingsContent.swift index e5bb26d61..339a8e7c6 100644 --- a/Where/WhereUI/Sources/Settings/SettingsContent.swift +++ b/Where/WhereUI/Sources/Settings/SettingsContent.swift @@ -19,7 +19,7 @@ extension EnvironmentValues { struct SettingsContent: View { let report: YearReportModel let recordingWarning: RecordingConfigurationWarningModel - @State private var navigation: SceneNavigationModel.Settings + @State private var navigation: SettingsView.Navigation @Environment(UpdateModel.self) private var updates: UpdateModel? @Environment(WhereSession.self) private var session @Environment(WhereModel.self) private var model @@ -30,13 +30,13 @@ struct SettingsContent: View { init( report: YearReportModel, recordingWarning: RecordingConfigurationWarningModel? = nil, - navigation: SceneNavigationModel.Settings? = nil, + navigation: SettingsView.Navigation? = nil, ) { self.report = report self .recordingWarning = recordingWarning ?? RecordingConfigurationWarningModel(preferences: report.preferences) - _navigation = State(initialValue: navigation ?? SceneNavigationModel.Settings()) + _navigation = State(initialValue: navigation ?? SettingsView.Navigation()) } private var searchQuery: String { diff --git a/Where/WhereUI/Sources/Settings/SettingsView.swift b/Where/WhereUI/Sources/Settings/SettingsView.swift index 79ddc4f3a..0cf42565f 100644 --- a/Where/WhereUI/Sources/Settings/SettingsView.swift +++ b/Where/WhereUI/Sources/Settings/SettingsView.swift @@ -1,3 +1,4 @@ +import Observation #if DEBUG @_spi(Testing) import InstallerKitUI #endif @@ -21,20 +22,44 @@ import WhereCore /// the `WhereSession` coordinator (recording/location) and /// `WhereModel` (reset) come from the environment via the sub-screens. struct SettingsView: View { + @MainActor @Observable + final class Navigation { + // Settings, evidence, and feature-gallery routes share the same stack. + // NavigationPath retains each value's concrete Hashable identity. + var path = NavigationPath() + private var route: SettingsRoute? + var compactColumn = NavigationSplitViewColumn.sidebar + var selectedRoute: SettingsRoute? { + get { route } + set { + guard route != newValue else { return } + route = newValue + path = NavigationPath() + if newValue != nil { compactColumn = .detail } + } + } + + var searchText = "" + var showRegions = false + } + let report: YearReportModel let recordingWarning: RecordingConfigurationWarningModel @State private var backup: BackupModel @State private var reminders: RemindersSettingsModel - @State private var navigation: SceneNavigationModel.Settings + @State private var navigation: SettingsView.Navigation @Environment(\.stylesheet) private var stylesheet init( report: YearReportModel, recordingWarning: RecordingConfigurationWarningModel? = nil, - navigation: SceneNavigationModel.Settings? = nil, + navigation: SettingsView.Navigation? = nil, + initialRoute: SettingsRoute? = nil, ) { - _navigation = State(initialValue: navigation ?? SceneNavigationModel.Settings()) + let navigation = navigation ?? SettingsView.Navigation() + if let initialRoute { navigation.selectedRoute = initialRoute } + _navigation = State(initialValue: navigation) self.report = report self.recordingWarning = recordingWarning ?? RecordingConfigurationWarningModel( preferences: report.preferences, diff --git a/Where/WhereUI/Sources/Year/YearView.swift b/Where/WhereUI/Sources/Year/YearView.swift index 37b82c0d0..7f424e5b3 100644 --- a/Where/WhereUI/Sources/Year/YearView.swift +++ b/Where/WhereUI/Sources/Year/YearView.swift @@ -1,3 +1,4 @@ +import BroadwayUI import SFSafeSymbols import SnapshotKit import SwiftUI @@ -9,24 +10,38 @@ import WhereCore struct YearView: View { let report: YearReportModel - @State private var navigation: SceneNavigationModel.Year + @State private var mode: YearMode + @State private var history: HistorySelectionModel + @State private var planningDestination: PlannedStaysDestination? + @State private var calendarPresentation: CalendarPresentationState @Environment(\.stylesheet) private var stylesheet - init(report: YearReportModel, navigation: SceneNavigationModel.Year) { + init( + report: YearReportModel, + initialMode: YearMode = .calendar, + history: HistorySelectionModel? = nil, + ) { self.report = report - _navigation = State(initialValue: navigation) - } - - init(report: YearReportModel, initialMode: YearMode = .calendar) { - self.init(report: report, navigation: SceneNavigationModel.Year(mode: initialMode)) + _mode = State(initialValue: initialMode) + _history = State(initialValue: history ?? HistorySelectionModel()) + _calendarPresentation = State(initialValue: CalendarPresentationState(mode: CalendarModel( + report: report, + persistenceKey: .year, + ).isOverview(fallback: false) ? .overview : .detail)) } var body: some View { AdaptiveHistoryContainer { arrangement in switch arrangement { case .compact: - YearCompactView(report: report, navigation: navigation) + YearCompactContent( + report: report, + mode: $mode, + history: history, + calendarPresentation: calendarPresentation, + planningDestination: $planningDestination, + ) case .columns: NavigationSplitView(columnVisibility: .constant(.all)) { timeline(presentation: .linked) @@ -51,22 +66,26 @@ struct YearView: View { .safeAreaInset(edge: .top, spacing: 0) { HistoryMonthControls( report: report, - history: navigation.history, + history: history, ) } } } } } - .modifier(PlanningPresentation(report: report, navigation: navigation.history.planning)) + .modifier(PlanningPresentation(report: report, destination: $planningDestination)) + .onChange(of: report.selectedYear) { history.reset() } } private func calendar(presentation: CalendarContentView.Presentation) -> some View { CalendarContentView( report: report, - history: navigation.history, + history: history, + presentationState: calendarPresentation, presentation: presentation, persistenceKey: .year, + scrollRole: .year, + planningDestination: $planningDestination, ) .navigationTitle(String(localized: .primaryCalendar)) .navigationBarTitleDisplayMode(.inline) @@ -76,7 +95,8 @@ struct YearView: View { PresenceTimelineList( report: report, presentation: presentation, - history: navigation.history, + history: history, + planningDestination: $planningDestination, ) .navigationTitle(String(localized: .primaryTimeline)) .navigationBarTitleDisplayMode(.inline) @@ -86,32 +106,64 @@ struct YearView: View { /// The single-pane production presentation, also captured in full for content completeness. struct YearCompactView: View { let report: YearReportModel - @State private var navigation: SceneNavigationModel.Year - @Environment(\.stylesheet) private var stylesheet + @State private var mode = YearMode.calendar + @State private var history = HistorySelectionModel() + @State private var planningDestination: PlannedStaysDestination? + @State private var calendarPresentation: CalendarPresentationState - init(report: YearReportModel, navigation: SceneNavigationModel.Year? = nil) { + init(report: YearReportModel) { self.report = report - _navigation = State(initialValue: navigation ?? SceneNavigationModel.Year()) + _calendarPresentation = State(initialValue: CalendarPresentationState(mode: CalendarModel( + report: report, + persistenceKey: .year, + ).isOverview(fallback: false) ? .overview : .detail)) } var body: some View { - @Bindable var navigation = navigation + YearCompactContent( + report: report, + mode: $mode, + history: history, + calendarPresentation: calendarPresentation, + planningDestination: $planningDestination, + ) + .bScrollRestorationScope() + .modifier(PlanningPresentation(report: report, destination: $planningDestination)) + } +} + +private struct YearCompactContent: View { + let report: YearReportModel + @Binding var mode: YearMode + let history: HistorySelectionModel + let calendarPresentation: CalendarPresentationState + @Binding var planningDestination: PlannedStaysDestination? + @Environment(\.stylesheet) private var stylesheet + + var body: some View { NavigationStack { Group { - switch navigation.mode { + switch mode { case .calendar: CalendarContentView( report: report, - history: navigation.history, + history: history, + presentationState: calendarPresentation, persistenceKey: .year, + scrollRole: .year, + planningDestination: $planningDestination, ) case .timeline: - PresenceTimelineList(report: report, history: navigation.history) + PresenceTimelineList( + report: report, + history: history, + planningDestination: $planningDestination, + ) } } // Crossfade between the two views rather than hard-cutting. - .animation(.default, value: navigation.mode) - .navigationTitle(navigation.mode.title) + .animation(.default, value: mode) + .navigationTitle(mode.title) .navigationBarTitleDisplayMode(.inline) // Keep the bar background on at all times. The calendar auto-scrolls // under the bar (so its scroll-edge material is showing) while the @@ -119,7 +171,7 @@ struct YearCompactView: View { // them animates that material in/out — reading as a toolbar fade. .toolbarBackground(.visible, for: .navigationBar) .safeAreaInset(edge: .bottom, alignment: .center) { - YearModePicker(mode: $navigation.mode) + YearModePicker(mode: $mode) .padding(.bottom, stylesheet.spacing.xLarge) } } diff --git a/Where/WhereUI/Tests/CalendarContentViewTests.swift b/Where/WhereUI/Tests/CalendarContentViewTests.swift index a966422a3..df60f4656 100644 --- a/Where/WhereUI/Tests/CalendarContentViewTests.swift +++ b/Where/WhereUI/Tests/CalendarContentViewTests.swift @@ -64,7 +64,8 @@ struct CalendarContentViewTests { } private func element(_ prefix: String, in view: UIView) -> NSObject? { - accessibilityObjects(in: view).first { $0.accessibilityLabel?.hasPrefix(prefix) == true } + HostedAccessibility.objects(in: view) + .first { $0.accessibilityLabel?.hasPrefix(prefix) == true } } private func isEnabled(_ label: String, in view: UIView) -> Bool { @@ -99,162 +100,341 @@ struct CalendarContentViewTests { } } - private func accessibilityObjects(in root: NSObject) -> [NSObject] { - var visited: Set = [] - func visit(_ object: NSObject) -> [NSObject] { - guard visited.insert(ObjectIdentifier(object)).inserted else { return [] } - var result = [object] - if let view = object as? UIView { - result += view.subviews.flatMap(visit) + @Test func overviewAndTabletopDoNotReplaceTheForecastAnchor() async throws { + let report = PreviewSupport.loadedYearReportModel() + let fixture = CalendarPresentationFixture() + let history = HistorySelectionModel() + let host = UIHostingController(rootView: CalendarPresentationHarness( + fixture: fixture, + report: report, + history: history, + )) + try await show(host) { controller in + controller.view.window?.layer.speed = 1 + try await waitForCalendar("initial forecast") { + guard let frame = element("Planned stays", in: host.view)?.accessibilityFrame + else { return false } + let bottom = host.view.convert(host.view.safeAreaLayoutGuide.layoutFrame, to: nil) + .maxY + - WhereStylesheet.default.locationForecast.padding + return abs(frame.maxY - bottom) < 1 } - let count = object.accessibilityElementCount() - if count > 0, count < 10000 { - for index in 0 ..< count { - if let child = object.accessibilityElement(at: index) as? NSObject { - result += visit(child) - } + let initialBottom = try #require(element("Planned stays", in: host.view)? + .accessibilityFrame.maxY) + try activate(#require(element("Show Year", in: host.view))) + try await waitForCalendar("overview transition starts") { + element("Show Months", in: host.view)?.accessibilityTraits + .contains(.notEnabled) == true + } + try await waitForCalendar("overview") { + isEnabled("Show Months", in: host.view) && isVisible( + "July", + in: host.view, + expanded: false, + ) + } + try activate(#require(element("Show Months", in: host.view))) + try await waitForCalendar("detail transition starts") { + element("Show Year", in: host.view)?.accessibilityTraits + .contains(.notEnabled) == true + } + do { + try await waitForCalendar("forecast after overview") { + isEnabled("Show Year", in: host.view) && element("Planned stays", in: host.view) + .map { abs($0.accessibilityFrame.maxY - initialBottom) < 1 } == true } + } catch { + throw TestHostError( + "\(error); expectedBottom=\(initialBottom), frame=\(String(describing: element("Planned stays", in: host.view)?.accessibilityFrame)), enabled=\(isEnabled("Show Year", in: host.view))", + ) } - return result + fixture.showsTabletop = true + try await waitForCalendar("tabletop month") { isVisible( + "July", + in: host.view, + expanded: true, + ) && element("Planned stays", in: host.view) == nil } + fixture.showsTabletop = false + try await waitForCalendar("forecast after tabletop") { element( + "Planned stays", + in: host.view, + ).map { abs($0.accessibilityFrame.maxY - initialBottom) < 1 } == true } } - return visit(root) } - @Test func repeatedLinkedSelectionExpandsOverviewAndKeepsMonthTarget() async throws { + @Test func theUnselectedLocationsCalendarKeepsItsSessionOnlyZoomMode() async throws { let report = PreviewSupport.loadedYearReportModel() - let history = HistoryNavigationModel() - let presentation = history.calendar.presentation( - report: report, - initialMode: .overview, - persistenceKey: .year, - ) + let state = AdaptiveHistoryHostState() + let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + LocationsView(report: report) + }.bScrollRestorationScope()) + host.safeAreaRegions = [] + try await show(host) { controller in + try await AdaptiveHistoryTestSupport.resize( + controller, + state: state, + to: .innerLandscape, + ) + try await waitForCalendar("unselected detail calendar") { isEnabled( + "Show Year", + in: host.view, + ) } + try activate(#require(element("Show Year", in: host.view))) + try await waitForCalendar("session-only overview") { isEnabled( + "Show Months", + in: host.view, + ) } + try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: .tabletop) + try await waitForCalendar("tabletop calendar") { isVisible( + "July", + in: host.view, + expanded: true, + ) } + try await AdaptiveHistoryTestSupport.resize( + controller, + state: state, + to: .innerLandscape, + ) + try await waitForCalendar("session-only overview after folding") { isEnabled( + "Show Months", + in: host.view, + ) } + #expect(report.preferences.calendarOverview(forKey: .year) == nil) + } + } + + @Test func changingRegionsResetsTheAnchorButKeepsEachZoomPreference() async throws { + let report = PreviewSupport.loadedYearReportModel() + report.preferences.setCalendarOverview(true, forKey: .region(.california)) + report.preferences.setCalendarOverview(false, forKey: .region(.newYork)) + let navigation = LocationsView.Navigation() + navigation.select(region: .california) + let state = AdaptiveHistoryHostState() + let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + LocationsView(report: report, navigation: navigation) + }.bScrollRestorationScope()) + host.safeAreaRegions = [] + try await show(host) { controller in + try await AdaptiveHistoryTestSupport.resize( + controller, + state: state, + to: .innerLandscape, + ) + try await waitForCalendar("California overview") { isEnabled( + "Show Months", + in: host.view, + ) } + try activate(#require(element("March", in: host.view))) + try await waitForCalendar("California March") { + isEnabled("Show Year", in: host.view) && isVisible( + "March", + in: host.view, + expanded: true, + ) + } + try activate(#require(element("Show Year", in: host.view))) + try await waitForCalendar("saved California overview") { isEnabled( + "Show Months", + in: host.view, + ) } + navigation.select(region: .newYork) + try await waitForCalendar("New York detail") { + isEnabled("Show Year", in: host.view) && isVisible( + "July", + in: host.view, + expanded: true, + ) + } + navigation.select(region: .california) + try await waitForCalendar("restored California overview preference") { isEnabled( + "Show Months", + in: host.view, + ) } + try activate(#require(element("Show Months", in: host.view))) + try await waitForCalendar("fresh California context") { isEnabled( + "Show Year", + in: host.view, + ) && isVisible("July", in: host.view, expanded: true) && !isVisible( + "March", + in: host.view, + expanded: true, + ) } + } + } + + @Test func removingTheForecastFallsBackToTheInitialMonth() async throws { + let report = PreviewSupport.loadedYearReportModel() + let host = UIHostingController(rootView: CalendarContentView(report: report) + .whereBroadwayRoot().bScrollRestorationScope()) + try await show(host) { _ in + try await waitForCalendar("initial forecast") { + element("Planned stays", in: host.view) != nil + } + try await report.setEstimatedTimeAndPlanningEnabled(false) + try await waitForCalendar("fallback after forecast removal") { element( + "Planned stays", + in: host.view, + ) == nil && isVisible("July", in: host.view, expanded: true) } + } + } + + @Test func aRevealWaitsForDelayedCalendarData() async throws { + let store = try TestStore() + let report = PlanningTestSupport.report(store: store) + let history = HistorySelectionModel() let month = CalendarDay(year: report.selectedYear, month: 2, day: 1) + history.select(month: month, dateCalendar: report.calendar) + await store.enableFirstSamplesGate() + let load = Task { await report.refresh() } + await store.awaitFirstSamplesCall() let host = UIHostingController(rootView: CalendarContentView( report: report, history: history, - persistenceKey: .year, - ).whereBroadwayRoot()) + ).whereBroadwayRoot().bScrollRestorationScope()) + do { + try await show(host) { _ in + #expect(history.pendingRequest(for: .calendar) != nil) + await store.releaseFirstSamplesCall() + await load.value + try await waitForCalendar("delayed February reveal") { + history.pendingRequest(for: .calendar) == nil && isVisible( + "February", + in: host.view, + expanded: true, + ) + } + } + } catch { + await store.releaseFirstSamplesCall() + await load.value + throw error + } + } + + @Test func repeatedLinkedSelectionExpandsOverviewAndKeepsMonthTarget() async throws { + let report = PreviewSupport.loadedYearReportModel() + let history = HistorySelectionModel() + let month = CalendarDay(year: report.selectedYear, month: 2, day: 1) + let host = UIHostingController(rootView: NavigationStack { + CalendarContentView( + report: report, + history: history, + initialMode: .overview, + persistenceKey: .year, + ) + }.whereBroadwayRoot().bScrollRestorationScope()) try await show(host) { _ in - try await waitForAsync { presentation.year == report.selectedYear } + try await waitForCalendar("overview to load") { isEnabled("Show Months", in: host.view) + } for _ in 0 ..< 2 { history.select(month: month, dateCalendar: report.calendar) - try await waitForAsync { - history.calendar.pendingReveal == nil && presentation.mode == .detail + try await waitForCalendar("explicit February reveal") { + history.pendingRequest(for: .calendar) == nil && isVisible("February", in: host.view, expanded: true) + && isEnabled("Show Year", in: host.view) } - presentation.toggle() - #expect(presentation.mode == .overview) + try activate(#require(element("Show Year", in: host.view))) + try await waitForCalendar("overview after reveal") { isEnabled( + "Show Months", + in: host.view, + ) } } } } - @Test func regionZoomPreferencesRemainIndependentInRetainedNavigation() { - let report = PreviewSupport.loadedYearReportModel() - let navigation = CalendarNavigationModel() - let california = navigation.presentation( - report: report, - initialMode: .detail, - persistenceKey: .region(.california), - ) - let newYork = navigation.presentation( - report: report, - initialMode: .detail, - persistenceKey: .region(.newYork), - ) - california.toggle() - #expect(california.mode == .overview) - #expect(newYork.mode == .detail) - #expect(navigation.presentation( - report: report, - initialMode: .detail, - persistenceKey: .region(.california), - ) === california) - } - @Test func mountedCalendarPositionsAfterTheNewYearsMonthsArrive() async throws { let report = PreviewSupport.loadedYearReportModel() - let navigation = CalendarNavigationModel() + let history = HistorySelectionModel() let host = UIHostingController(rootView: CalendarContentView( report: report, - navigation: navigation, + history: history, ) - .whereBroadwayRoot()) + .whereBroadwayRoot().bScrollRestorationScope()) try await show(host) { _ in - try await waitForAsync { navigation.initiallyPositionedYear == report.selectedYear } - let previousYear = report.selectedYear - 1 - await report.select(year: previousYear) - try await waitForAsync { - navigation.initiallyPositionedYear == previousYear - && isVisible("December", in: host.view, expanded: true) + try await waitForCalendar("initial forecast") { + element("Planned stays", in: host.view) != nil } - #expect(navigation.scrollPosition.viewID(type: CalendarDay.self) == CalendarDay( - year: previousYear, - month: 12, - day: 1, - )) + history.select( + month: CalendarDay(year: report.selectedYear, month: 2, day: 1), + dateCalendar: report.calendar, + ) + try await waitForCalendar("February before year change") { isVisible( + "February", + in: host.view, + expanded: true, + ) } + await report.select(year: report.selectedYear - 1) + try await waitForCalendar("December after year change") { isVisible( + "December", + in: host.view, + expanded: true, + ) } } } @Test func aRemountedCalendarPreservesTheInitialEstimateAlignment() async throws { let report = PreviewSupport.loadedYearReportModel() - let navigation = CalendarNavigationModel() - let first = UIHostingController(rootView: CalendarContentView( - report: report, - navigation: navigation, - ).whereBroadwayRoot()) + let store = BScrollRestorationStore() + let first = UIHostingController(rootView: CalendarContentView(report: report) + .whereBroadwayRoot().bScrollRestorationScope(store)) var initialBottom: CGFloat? try await show(first) { _ in try await waitForCalendar("initial estimate to reach the viewport bottom") { guard let frame = element("Planned stays", in: first.view)?.accessibilityFrame else { return false } - let viewport = first.view.convert(first.view.bounds, to: nil) - return viewport.contains(frame) && frame.minY > viewport.midY + let bottom = first.view.convert(first.view.safeAreaLayoutGuide.layoutFrame, to: nil) + .maxY + - WhereStylesheet.default.locationForecast.padding + return abs(frame.maxY - bottom) < 1 } initialBottom = element("Planned stays", in: first.view)?.accessibilityFrame.maxY } let expectedBottom = try #require(initialBottom) - let second = UIHostingController(rootView: CalendarContentView( - report: report, - navigation: navigation, - ).whereBroadwayRoot()) + let second = UIHostingController(rootView: CalendarContentView(report: report) + .whereBroadwayRoot().bScrollRestorationScope(store)) try await show(second) { _ in try await waitForCalendar("remounted estimate to retain its bottom alignment") { guard let bottom = element("Planned stays", in: second.view)?.accessibilityFrame - .maxY - else { return false } + .maxY else { return false } return abs(bottom - expectedBottom) <= 1 } } } - @Test func aRemountedCalendarUsesTheSceneAnchor() async throws { + @Test func aRemountedCalendarUsesItsRenderedMonthAnchor() async throws { let report = PreviewSupport.loadedYearReportModel() - let scene = SceneNavigationModel() - let navigation = scene.year.calendar - let browsed = CalendarDay(year: report.selectedYear, month: 2, day: 1) + let history = HistorySelectionModel() + let store = BScrollRestorationStore() let first = UIHostingController(rootView: CalendarContentView( report: report, - navigation: navigation, + history: history, ) - .whereBroadwayRoot()) + .whereBroadwayRoot().bScrollRestorationScope(store)) try await show(first) { _ in - try await waitForAsync { - navigation.initiallyPositionedYear == report.selectedYear - } + try await waitForCalendar("initial forecast") { element( + "Planned stays", + in: first.view, + ) != nil } + history.select( + month: CalendarDay(year: report.selectedYear, month: 2, day: 1), + dateCalendar: report.calendar, + ) + try await waitForCalendar("rendered February anchor") { isVisible( + "February", + in: first.view, + expanded: true, + ) } } - navigation.recordVisibleMonths([browsed]) - navigation.presentation(report: report, initialMode: .detail, persistenceKey: nil) - .remember(.month(browsed)) - let second = UIHostingController(rootView: CalendarContentView( - report: report, - navigation: navigation, - ) - .whereBroadwayRoot()) + // A new coordinator has no reveal request: only the retained native position can restore + // February. + let second = UIHostingController(rootView: CalendarContentView(report: report) + .whereBroadwayRoot().bScrollRestorationScope(store)) try await show(second) { _ in - try await waitForAsync { - isVisible("February", in: second.view, expanded: true) - } - #expect(scene.locations.calendar.initiallyPositionedYear == nil) + try await waitForCalendar("restored February anchor") { isVisible( + "February", + in: second.view, + expanded: true, + ) } } } } diff --git a/Where/WhereUI/Tests/CalendarModelTests.swift b/Where/WhereUI/Tests/CalendarModelTests.swift new file mode 100644 index 000000000..409d7aa9a --- /dev/null +++ b/Where/WhereUI/Tests/CalendarModelTests.swift @@ -0,0 +1,46 @@ +import Testing +import WhereCore +@testable import WhereUI + +@MainActor +struct CalendarModelTests { + @Test func preferencesRemainIndependentAcrossRecreationAndYearChanges() async { + let report = PreviewSupport.loadedYearReportModel() + let california = CalendarModel(report: report, persistenceKey: .region(.california)) + let newYork = CalendarModel(report: report, persistenceKey: .region(.newYork)) + california.setOverview(true) + newYork.setOverview(false) + #expect(CalendarModel(report: report, persistenceKey: .region(.california)) + .isOverview(fallback: false)) + await report.select(year: report.selectedYear - 1) + #expect(california.isOverview(fallback: false)) + #expect(!newYork.isOverview(fallback: true)) + let sessionOnly = CalendarModel(report: report, persistenceKey: nil) + sessionOnly.setOverview(true) + #expect(!sessionOnly.isOverview(fallback: false)) + } + + @Test func overviewAvailabilityMatchesDetailedPlanningVisibility() throws { + let report = PreviewSupport.plannedStayYearReportModel() + let yearReport = try #require(report.report) + let months = try yearReport.calendarMonths( + calendar: report.calendar, + referenceDate: report.referenceDate, + flightDays: [], + ) + let visible = CalendarModel.visibleMonths( + months, + report: report, + focusedRegion: nil, + ) + #expect(months.count == 12) + #expect(visible.contains { $0.month > 7 }) + let hidden = PreviewSupport.plannedStayYearReportModel(showsEstimatedTimeAndPlanning: false) + let hiddenMonths = CalendarModel.visibleMonths( + months, + report: hidden, + focusedRegion: nil, + ) + #expect(hiddenMonths.map(\.month) == Array(1 ... 7)) + } +} diff --git a/Where/WhereUI/Tests/CalendarPresentationModelTests.swift b/Where/WhereUI/Tests/CalendarPresentationModelTests.swift deleted file mode 100644 index afbc1e92f..000000000 --- a/Where/WhereUI/Tests/CalendarPresentationModelTests.swift +++ /dev/null @@ -1,247 +0,0 @@ -import Testing -import WhereCore -@testable import WhereUI - -@MainActor -struct CalendarPresentationModelTests { - private func month(_ number: Int, year: Int = 2026) -> CalendarDay { - CalendarDay(year: year, month: number, day: 1) - } - - private func model( - preferences: WherePreferences = makePreferences(), - persistenceKey: CalendarZoomKey? = nil, - ) -> CalendarPresentationModel { - let model = CalendarPresentationModel( - mode: .detail, - preferences: preferences, - persistenceKey: persistenceKey, - ) - model.reconcile( - year: 2026, - availableMonths: (1 ... 10).map { month($0) }, - currentMonth: month(7), - estimateMonth: month(7), - ) - return model - } - - @Test(arguments: [false, true]) - func savedZoomOverridesInitialModeAcrossRecreation(overview: Bool) { - let store = RecordingKeyValueStore() - WherePreferences(store: store).setCalendarOverview(overview, forKey: .year) - - let restored = CalendarPresentationModel( - mode: overview ? .detail : .overview, - preferences: WherePreferences(store: store), - persistenceKey: .year, - ) - - #expect(restored.mode == (overview ? .overview : .detail)) - #expect(store.writeCount == 1) - } - - @Test func missingPreferenceUsesInitialModeWithoutWritingIt() { - let store = RecordingKeyValueStore() - let model = CalendarPresentationModel( - mode: .overview, - preferences: WherePreferences(store: store), - persistenceKey: .year, - ) - #expect(model.mode == .overview) - #expect(store.writeCount == 0) - } - - @Test func nilKeyNeverReadsOrWritesPreferences() { - let store = RecordingKeyValueStore() - let model = model(preferences: WherePreferences(store: store), persistenceKey: nil) - model.toggle() - #expect(model.mode == .overview) - model.expand(.estimate(month(7))) - model.finishPinch(magnification: 0.8, destination: nil) - model.finishPinch(magnification: 1.2, destination: .month(month(5))) - #expect(model.mode == .detail) - #expect(store.readCount == 0) - #expect(store.writeCount == 0) - } - - @Test func everyZoomIntentPersistsOnlyItsCalendar() { - let preferences = makePreferences() - preferences.setCalendarOverview(false, forKey: .year) - preferences.setCalendarOverview(true, forKey: .region(.california)) - let key = CalendarZoomKey.region(.newYork) - let model = model(preferences: preferences, persistenceKey: key) - - model.finishPinch(magnification: 0.8, destination: nil) - #expect(preferences.calendarOverview(forKey: key) == true) - model.finishPinch(magnification: 1.2, destination: .month(month(9))) - #expect(preferences.calendarOverview(forKey: key) == false) - model.toggle() - #expect(preferences.calendarOverview(forKey: key) == true) - model.expand(.estimate(month(7))) - #expect(preferences.calendarOverview(forKey: key) == false) - model.toggle() - model.expand(.month(month(5))) - #expect(preferences.calendarOverview(forKey: key) == false) - - #expect(preferences.calendarOverview(forKey: .year) == false) - #expect(preferences.calendarOverview(forKey: .region(.california)) == true) - } - - @Test func rejectedAndUnchangedIntentsDoNotWritePreferences() { - let store = RecordingKeyValueStore() - let model = model(preferences: WherePreferences(store: store), persistenceKey: .year) - model.expand(.month(month(5))) - model.finishPinch(magnification: 0.81, destination: nil) - #expect(store.writeCount == 0) - - model.toggle() - #expect(store.writeCount == 1) - model.expand(.month(month(11))) - model.finishPinch(magnification: 1.19, destination: .month(month(5))) - model.reconcile( - year: 2025, - availableMonths: [month(12, year: 2025)], - currentMonth: nil, - estimateMonth: nil, - ) - #expect(model.mode == .overview) - #expect(store.writeCount == 1) - } - - @Test func pinchThresholdsAndRestoration() { - let model = model() - model.remember(.month(month(5))) - model.finishPinch(magnification: 0.81, destination: nil) - #expect(model.mode == .detail) - model.finishPinch(magnification: 0.8, destination: nil) - #expect(model.mode == .overview) - model.finishPinch(magnification: 1.19, destination: .month(month(9))) - #expect(model.mode == .overview) - model.toggle() - #expect(model.target == .month(month(5))) - model.toggle() - model.finishPinch(magnification: 1.2, destination: .month(month(9))) - #expect(model.mode == .detail) - #expect(model.target == .month(month(9))) - } - - @Test func unavailableMonthRejectsButPlannedFutureMonthExpands() { - let model = model() - model.toggle() - #expect(!model.expand(.month(month(11)))) - #expect(model.mode == .overview) - #expect(model.rejectedTarget == .month(month(11))) - #expect(model.rejectionCount == 1) - model.finishPinch(magnification: 1.3, destination: .month(month(11))) - #expect(model.rejectionCount == 2) - #expect(model.expand(.month(month(10)))) - #expect(model.target == .month(month(10))) - } - - @Test func estimateRestoresAndReconcilesWhenHidden() { - let model = model() - model.toggle() - #expect(model.expand(.estimate(month(7)))) - #expect(model.target == .estimate(month(7))) - model.reconcile( - year: 2026, - availableMonths: (1 ... 7).map { month($0) }, - currentMonth: month(7), - estimateMonth: nil, - ) - #expect(model.target == .month(month(7))) - #expect(!model.isAvailable(.estimate(month(7)))) - } - - @Test func yearChangePreservesZoomAndUsesLastAvailableMonth() { - let model = model() - model.toggle() - model.reconcile( - year: 2025, - availableMonths: (1 ... 12).map { month($0, year: 2025) }, - currentMonth: nil, - estimateMonth: nil, - ) - #expect(model.mode == .overview) - #expect(model.target == .month(month(12, year: 2025))) - model.reconcile(year: 2027, availableMonths: [], currentMonth: nil, estimateMonth: nil) - #expect(model.target == nil) - #expect(!model.expand(nil)) - } - - @Test func initialEstimateAlignmentSurvivesReconciliationUntilSuperseded() { - let model = model() - model.prepareInitialPosition() - model.reconcile( - year: 2026, - availableMonths: (1 ... 10).map { month($0) }, - currentMonth: month(7), - estimateMonth: month(7), - ) - #expect(model.initialBottomTarget == .estimate(month(7))) - model.expand(.month(month(5))) - #expect(model.initialBottomTarget == nil) - #expect(model.target == .month(month(5))) - } - - @Test func browsingAndZoomReplaceInitialBottomAlignment() { - let model = model() - model.prepareInitialPosition() - model.remember(.month(month(5))) - #expect(model.initialBottomTarget == nil) - model.prepareInitialPosition() - model.toggle() - #expect(model.initialBottomTarget == nil) - model.expand(.estimate(month(7))) - #expect(model.initialBottomTarget == nil) - model.prepareInitialPosition() - model.finishPinch(magnification: 0.5, destination: nil) - #expect(model.initialBottomTarget == nil) - } - - @Test func removedEstimateAndNewYearDiscardInitialBottomAlignment() { - let model = model() - model.prepareInitialPosition() - model.reconcile( - year: 2026, - availableMonths: (1 ... 7).map { month($0) }, - currentMonth: month(7), - estimateMonth: nil, - ) - #expect(model.initialBottomTarget == nil) - model.prepareInitialPosition() - #expect(model.initialBottomTarget == .month(month(7))) - model.reconcile( - year: 2025, - availableMonths: [month(12, year: 2025)], - currentMonth: nil, - estimateMonth: nil, - ) - #expect(model.initialBottomTarget == nil) - } - - @Test func overviewAvailabilityMatchesDetailedPlanningVisibility() throws { - let report = PreviewSupport.plannedStayYearReportModel() - let yearReport = try #require(report.report) - let months = try yearReport.calendarMonths( - calendar: report.calendar, - referenceDate: report.referenceDate, - flightDays: [], - ) - let visible = CalendarPresentationModel.visibleMonths( - months, - report: report, - focusedRegion: nil, - ) - #expect(months.count == 12) - #expect(visible.contains { $0.month > 7 }) - let hidden = PreviewSupport.plannedStayYearReportModel(showsEstimatedTimeAndPlanning: false) - let hiddenMonths = CalendarPresentationModel.visibleMonths( - months, - report: hidden, - focusedRegion: nil, - ) - #expect(hiddenMonths.map(\.month) == Array(1 ... 7)) - } -} diff --git a/Where/WhereUI/Tests/CalendarPresentationStateTests.swift b/Where/WhereUI/Tests/CalendarPresentationStateTests.swift new file mode 100644 index 000000000..9686554e4 --- /dev/null +++ b/Where/WhereUI/Tests/CalendarPresentationStateTests.swift @@ -0,0 +1,20 @@ +import Testing +import WhereCore +@testable import WhereUI + +@MainActor +struct CalendarPresentationStateTests { + @Test func unavailableDestinationsRejectWithoutChangingMode() { + let state = CalendarPresentationState(mode: .overview) + let july = CalendarDay(year: 2026, month: 7, day: 1) + state.reconcile(year: 2026, availableMonths: [july], estimateMonth: nil) + #expect(!state.expand(.estimate(july))) + #expect(state.mode == .overview) + #expect(state.rejectionCount == 1) + #expect(state.expand(.month(july))) + state.mode = .overview + state.reconcile(year: 2025, availableMonths: [], estimateMonth: nil) + #expect(state.mode == .overview) + #expect(state.rejectedTarget == nil) + } +} diff --git a/Where/WhereUI/Tests/CalendarZoomTransitionModelTests.swift b/Where/WhereUI/Tests/CalendarZoomTransitionStateTests.swift similarity index 83% rename from Where/WhereUI/Tests/CalendarZoomTransitionModelTests.swift rename to Where/WhereUI/Tests/CalendarZoomTransitionStateTests.swift index bda89813f..eaa801097 100644 --- a/Where/WhereUI/Tests/CalendarZoomTransitionModelTests.swift +++ b/Where/WhereUI/Tests/CalendarZoomTransitionStateTests.swift @@ -4,24 +4,24 @@ import WhereCore @testable import WhereUI @MainActor -struct CalendarZoomTransitionModelTests { +struct CalendarZoomTransitionStateTests { private let viewport = CGRect(x: 0, y: 0, width: 402, height: 800) private let content = CGRect(x: 0, y: 0, width: 402, height: 2000) private let compact = CGRect(x: 140, y: 420, width: 120, height: 100) private let expanded = CGRect(x: 16, y: 0, width: 370, height: 360) - private let november = CalendarPresentationModel.Target.month(CalendarDay( + private let november = CalendarPresentationState.Target.month(CalendarDay( year: 2026, month: 11, day: 1, )) - private let december = CalendarPresentationModel.Target.month(CalendarDay( + private let december = CalendarPresentationState.Target.month(CalendarDay( year: 2026, month: 12, day: 1, )) @Test func waitsForTheSelectedMonthToFinishScrollingBeforeMatching() throws { - let model = CalendarZoomTransitionModel() + let model = CalendarZoomTransitionState() model.begin( from: .init( mode: .overview, @@ -61,8 +61,31 @@ struct CalendarZoomTransitionModelTests { #expect(tile.destination == expanded) } + @Test func aRestoredCenteredAnchorCanFinishTheTransition() throws { + let state = CalendarZoomTransitionState() + state.begin( + from: .init( + mode: .overview, + frames: [november: compact], + viewport: viewport, + content: content, + ), + to: .detail, + focus: november, + alignment: .center, + ) + let centered = expanded.offsetBy(dx: 0, dy: (viewport.height - expanded.height) / 2) + state.resolve(destination: .init( + mode: .detail, + frames: [november: centered], + viewport: viewport, + content: content, + )) + #expect(try movingPlan(state).tiles.first?.destination == centered) + } + @Test func matchesBothVisibleMonthsWhenZoomingOut() throws { - let model = CalendarZoomTransitionModel() + let model = CalendarZoomTransitionState() let decemberDetail = expanded.offsetBy(dx: 0, dy: 376) let decemberOverview = compact.offsetBy(dx: 128, dy: 0) model.begin( @@ -90,23 +113,23 @@ struct CalendarZoomTransitionModelTests { #expect(plan.tiles.allSatisfy { $0.visibility == .both }) } - @Test(arguments: [CalendarPresentationModel.Mode.detail, .overview]) - func projectedFramesCannotIntroduceAnOffscreenMonth(destinationMode: CalendarPresentationModel + @Test(arguments: [CalendarPresentationState.Mode.detail, .overview]) + func projectedFramesCannotIntroduceAnOffscreenMonth(destinationMode: CalendarPresentationState .Mode) throws { - let august = CalendarPresentationModel.Target.month(CalendarDay( + let august = CalendarPresentationState.Target.month(CalendarDay( year: 2026, month: 8, day: 1, )) - let overview = CalendarZoomTransitionModel.Measurement( + let overview = CalendarZoomTransitionState.Measurement( mode: .overview, frames: [november: compact, august: compact.offsetBy(dx: 0, dy: -108)], viewport: viewport, content: content, ) - let detail = CalendarZoomTransitionModel.Measurement( + let detail = CalendarZoomTransitionState.Measurement( mode: .detail, frames: [ november: expanded.offsetBy(dx: 0, dy: 400), @@ -115,7 +138,7 @@ struct CalendarZoomTransitionModelTests { viewport: viewport, content: content.offsetBy(dx: 0, dy: -1200), ) - let model = CalendarZoomTransitionModel() + let model = CalendarZoomTransitionState() model.begin( from: destinationMode == .detail ? overview : detail, to: destinationMode, @@ -132,7 +155,7 @@ struct CalendarZoomTransitionModelTests { func offscreenCounterpartsContinueTheSelectedMonthsScaleAndTranslation( hasOffscreenFrame: Bool, ) throws { - let model = CalendarZoomTransitionModel() + let model = CalendarZoomTransitionState() let decemberOverview = compact.offsetBy(dx: 128, dy: 0) model.begin( from: .init( @@ -161,8 +184,8 @@ struct CalendarZoomTransitionModelTests { } @Test func estimateCanSettleAgainstTheBottomInsteadOfTheTop() throws { - let model = CalendarZoomTransitionModel() - let estimate = CalendarPresentationModel.Target.estimate(CalendarDay( + let model = CalendarZoomTransitionState() + let estimate = CalendarPresentationState.Target.estimate(CalendarDay( year: 2026, month: 7, day: 1, @@ -194,8 +217,8 @@ struct CalendarZoomTransitionModelTests { } @Test func staleCompletionCannotEndANewerTransition() throws { - let model = CalendarZoomTransitionModel() - let measurement = CalendarZoomTransitionModel.Measurement( + let model = CalendarZoomTransitionState() + let measurement = CalendarZoomTransitionState.Measurement( mode: .overview, frames: [november: compact], viewport: viewport, @@ -214,7 +237,7 @@ struct CalendarZoomTransitionModelTests { @Test(arguments: [false, true]) func resizingCancelsInsteadOfAnimatingStaleCoordinates(alreadyMoving: Bool) { - let model = CalendarZoomTransitionModel() + let model = CalendarZoomTransitionState() model.begin( from: .init( mode: .overview, @@ -247,7 +270,7 @@ struct CalendarZoomTransitionModelTests { } @Test func disjointLazyLayoutsReleaseTheOverlay() { - let model = CalendarZoomTransitionModel() + let model = CalendarZoomTransitionState() model.begin( from: .init( mode: .detail, @@ -270,8 +293,8 @@ struct CalendarZoomTransitionModelTests { } @Test func unrelatedLayoutCallbacksCannotStartTheFlight() { - let model = CalendarZoomTransitionModel() - let overview = CalendarZoomTransitionModel.Measurement( + let model = CalendarZoomTransitionState() + let overview = CalendarZoomTransitionState.Measurement( mode: .overview, frames: [november: compact], viewport: viewport, @@ -286,7 +309,7 @@ struct CalendarZoomTransitionModelTests { } @Test func subpixelViewportRoundingDoesNotCancelMatching() throws { - let model = CalendarZoomTransitionModel() + let model = CalendarZoomTransitionState() model.begin( from: .init( mode: .overview, @@ -311,7 +334,7 @@ struct CalendarZoomTransitionModelTests { } @Test func waitsForIncomingMeasurementsAfterOutgoingAnchorsDisappear() { - let model = CalendarZoomTransitionModel() + let model = CalendarZoomTransitionState() model.begin( from: .init( mode: .detail, @@ -332,8 +355,8 @@ struct CalendarZoomTransitionModelTests { } @Test func offscreenEstimateEntersAtFullWidthBelowTheGrid() throws { - let model = CalendarZoomTransitionModel() - let estimate = CalendarPresentationModel.Target.estimate(CalendarDay( + let model = CalendarZoomTransitionState() + let estimate = CalendarPresentationState.Target.estimate(CalendarDay( year: 2026, month: 7, day: 1, @@ -364,10 +387,10 @@ struct CalendarZoomTransitionModelTests { #expect(tile.destination == destination) } - private func movingPlan(_ model: CalendarZoomTransitionModel) throws - -> CalendarZoomTransitionModel.Plan + private func movingPlan(_ model: CalendarZoomTransitionState) throws + -> CalendarZoomTransitionState.Plan { - try #require({ () -> CalendarZoomTransitionModel.Plan? in + try #require({ () -> CalendarZoomTransitionState.Plan? in guard case let .moving(plan) = model.phase else { return nil } return plan }()) diff --git a/Where/WhereUI/Tests/LocationsViewTests.swift b/Where/WhereUI/Tests/LocationsViewTests.swift index 194147dc3..85102579a 100644 --- a/Where/WhereUI/Tests/LocationsViewTests.swift +++ b/Where/WhereUI/Tests/LocationsViewTests.swift @@ -1,3 +1,6 @@ +import BroadwayUI +import RegionKit +import SnapshotKit import SwiftUI import TestHostSupport import Testing @@ -10,17 +13,27 @@ struct LocationsViewTests { } } -/// Both tests resize native navigation in the shared test host window. +/// These tests resize native navigation in the shared test host window. @MainActor @Suite(.serialized) struct LocationsViewTestsDuo { @Test func initialBrowsePositionFollowsInsetsUntilARegionIsRestored() async throws { let world = try await WhereFlyoverWorld.build() - let navigation = SceneNavigationModel.Locations() + let navigation = LocationsView.Navigation() + let store = BScrollRestorationStore() + let capture = ScrollPositionCapture() let host = UIHostingController(rootView: LocationsBrowsePane( report: world.report, navigation: navigation, ) + .overlay { ScrollPositionProbe( + LocationsCardsView.self, + role: LocationsCardsView.ScrollRole.browse, + context: world.report.selectedYear, + idType: Region.self, + capture: capture, + ) } + .bScrollRestorationScope(store) .dynamicTypeSize(.accessibility5) .whereBroadwayRoot()) @@ -28,32 +41,117 @@ struct LocationsViewTestsDuo { controller.view.frame = CGRect(x: 0, y: 0, width: 466, height: 678) controller.traitOverrides.horizontalSizeClass = .compact controller.view.layoutIfNeeded() - try await waitForAsync { + try await waitForStage("initial scroll content") { AdaptiveHistoryTestSupport.descendants( of: controller.view, matching: UIScrollView.self, ).contains { $0.contentSize.height > $0.bounds.height } } - let scroll = try #require(AdaptiveHistoryTestSupport.descendants( + var scroll = try #require(AdaptiveHistoryTestSupport.descendants( of: controller.view, matching: UIScrollView.self, ).first { $0.contentSize.height > $0.bounds.height }) + @MainActor func currentScroll() -> UIScrollView? { + AdaptiveHistoryTestSupport.descendants( + of: controller.view, + matching: UIScrollView.self, + ) + .first { $0.window != nil && $0.contentSize.height > $0.bounds.height } + } for topInset in [CGFloat(82), 0, 56, 0] { let previousInset = scroll.adjustedContentInset.top controller.additionalSafeAreaInsets.top = topInset - controller.view.layoutIfNeeded() - try await waitForAsync { - abs(scroll.adjustedContentInset.top - previousInset) > 1 - && abs(scroll.contentOffset.y + scroll.adjustedContentInset.top) < 1 + controller.view.setNeedsLayout() + controller.view.window?.layoutIfNeeded() + do { + try await waitForStage( + "top anchoring after inset \(previousInset) -> requested \(topInset)", + ) { + guard let current = currentScroll() else { return false } + return abs(current.adjustedContentInset.top - previousInset) > 1 + && abs(current.contentOffset.y + current.adjustedContentInset.top) < 1 + } + } catch { + throw TestHostError( + "\(error); adjusted=\(scroll.adjustedContentInset), offset=\(scroll.contentOffset), bounds=\(scroll.bounds), content=\(scroll.contentSize), attached=\(scroll.window != nil), current=\(String(describing: currentScroll()))", + ) } + let current = try #require(currentScroll()) + scroll = current } - navigation.visibleRegion = .california - navigation.restoreBrowsePosition() - try await waitForAsync { + let position = try #require(capture.binding) + position.wrappedValue.scrollTo(id: Region.california, anchor: .top) + try await waitForStage("browse to California") { scroll.contentOffset.y + scroll.adjustedContentInset.top > 100 } #expect(navigation.selectedRegion == nil) } + let restored = UIHostingController(rootView: LocationsBrowsePane(report: world.report) + .dynamicTypeSize(.accessibility5).whereBroadwayRoot().bScrollRestorationScope(store)) + try await show(restored) { controller in + controller.view.frame = CGRect(x: 0, y: 0, width: 466, height: 678) + controller.view.layoutIfNeeded() + try await waitForStage("restored California after pane replacement") { + AdaptiveHistoryTestSupport.descendants( + of: controller.view, + matching: UIScrollView.self, + ) + .contains { + $0.contentSize.height > $0.bounds.height && $0.contentOffset.y + $0 + .adjustedContentInset.top > 100 + } + } + } + } + + @Test func aPushedBrowseStackRemainsMountedThroughTabletop() async throws { + let world = try await WhereFlyoverWorld.build() + let navigation = LocationsView.Navigation() + let state = AdaptiveHistoryHostState() + let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + LocationsView(report: world.report, navigation: navigation) + .environment(world.session).environment(world.model) + }.bScrollRestorationScope()) + host.safeAreaRegions = [] + @MainActor func controllers(_ root: UIViewController) -> [UIViewController] { + [root] + root.children.flatMap(controllers) + } + try await show(host) { controller in + controller.view.window?.layer.speed = 1 + try await AdaptiveHistoryTestSupport.resize( + controller, + state: state, + to: .outerPortrait, + ) + try await waitForStage("browse stack to mount") { + controllers(controller).contains { $0 is UINavigationController } + } + navigation.open(.flight(PreviewSupport.flightReview(state: .ready))) + do { + try await waitForStage("flight detail push") { + controllers(controller).compactMap { $0 as? UINavigationController } + .contains { $0.viewControllers.count > 1 } + } + } catch { + throw TestHostError( + "\(error); path=\(navigation.browsePath.count); controllers=\(controllers(controller).map { String(describing: type(of: $0)) + ":" + (($0 as? UINavigationController)?.navigationBar.topItem?.title ?? $0.title ?? "nil") + ":" + String(($0 as? UINavigationController)?.viewControllers.count ?? 0) })", + ) + } + let stack = try #require(controllers(controller) + .compactMap { $0 as? UINavigationController } + .first { $0.viewControllers.count > 1 }) + let pushed = try #require(stack.topViewController) + for pose in [SnapshotConfiguration.Pose.innerPortrait, .tabletop, .outerPortrait] { + try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) + #expect(navigation.browsePath.count == 1) + #expect(controllers(controller).contains { $0 === pushed }) + } + } + } + + private func waitForStage(_ stage: String, _ predicate: () -> Bool) async throws { + do { try await waitForAsync(predicate: predicate) } + catch { throw TestHostError("Locations stage: \(stage): \(error)") } } @Test func foldingDuringLoadingAndAYearChangeDoesNotRestartOrPublishTheOlderRead() async throws { @@ -73,7 +171,7 @@ struct LocationsViewTestsDuo { await store.enableFirstSamplesGate() let olderRead = Task { await report.select(year: 2024) } await store.awaitFirstSamplesCall() - let navigation = SceneNavigationModel.Locations() + let navigation = LocationsView.Navigation() navigation.select(region: .california) let state = AdaptiveHistoryHostState() let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { @@ -84,7 +182,7 @@ struct LocationsViewTestsDuo { try await show(host) { controller in let readsBeforeFolding = await store.sampleReadCount for pose in [ - AdaptiveHistoryPose.outerPortrait, + SnapshotConfiguration.Pose.outerPortrait, .innerPortrait, .tabletop, .outerPortrait, @@ -95,7 +193,7 @@ struct LocationsViewTestsDuo { } await report.select(year: 2026) let readsAfterYearChange = await store.sampleReadCount - for pose in [AdaptiveHistoryPose.innerLandscape, .tabletop, .outerPortrait] { + for pose in [SnapshotConfiguration.Pose.innerLandscape, .tabletop, .outerPortrait] { try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) #expect(await store.sampleReadCount == readsAfterYearChange) } diff --git a/Where/WhereUI/Tests/MainTabsTests.swift b/Where/WhereUI/Tests/MainTabsTests.swift index 1057b35d7..48b154474 100644 --- a/Where/WhereUI/Tests/MainTabsTests.swift +++ b/Where/WhereUI/Tests/MainTabsTests.swift @@ -1,4 +1,6 @@ +import BroadwayUI import InstallerKit +import SnapshotKit @_spi(Testing) import InstallerKitUI import SwiftUI import TestHostSupport @@ -77,19 +79,18 @@ struct MainTabsTests { struct MainTabsTestsDuo { @Test func settingsSearchStaysVisibleAcrossFoldLayouts() async throws { let world = try await WhereFlyoverWorld.build() - let navigation = SceneNavigationModel() - navigation.selectedTab = .settings let state = AdaptiveHistoryHostState() let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { - MainTabs(world: world, navigation: navigation) + MainTabs(world: world, tab: .settings) .environment(world.model) .environment(world.session) .environment(\.isInDemoMode, true) }) host.safeAreaRegions = [] try await show(host) { controller in + var enteredQuery = false for pose in [ - AdaptiveHistoryPose.outerPortrait, + SnapshotConfiguration.Pose.outerPortrait, .innerPortrait, .tabletop, .book, @@ -113,16 +114,27 @@ struct MainTabsTestsDuo { .contains(frame) } } + let field = try #require(AdaptiveHistoryTestSupport.descendants( + of: window, + matching: UISearchTextField.self, + ).first { !$0.isHidden && $0.window != nil }) + if !enteredQuery { + field.text = "appearance" + field.sendActions(for: .editingChanged) + enteredQuery = true + } + #expect(field.text == "appearance") } } } @Test func foldingKeepsOnePresentedEditorAndItsUnsavedDate() async throws { let world = try await WhereFlyoverWorld.build() - let navigation = SceneNavigationModel() + let navigation = LocationsView.Navigation() let state = AdaptiveHistoryHostState() let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { - MainTabs(world: world, navigation: navigation) + LocationsView(report: world.report, navigation: navigation) + .bScrollRestorationScope() .environment(world.model) .environment(world.session) .environment(\.isInDemoMode, true) @@ -134,12 +146,8 @@ struct MainTabsTestsDuo { state: state, to: .outerPortrait, ) - navigation.locations.select(region: .california) - let month = CalendarDay(year: world.report.selectedYear, month: 2, day: 1) - navigation.locations.calendar.reveal(month: month) - navigation.settings.searchText = "appearance" - navigation.year.mode = .timeline - navigation.locations.planningDestination = .new(.california) + navigation.select(region: .california) + navigation.planningDestination = .new(.california) let root = try #require(controller.view.window?.rootViewController) try await waitForAsync { AdaptiveHistoryTestSupport.presentedController(from: root) != nil @@ -159,7 +167,7 @@ struct MainTabsTestsDuo { picker.date = draft picker.sendActions(for: .valueChanged) for pose in [ - AdaptiveHistoryPose.innerPortrait, + SnapshotConfiguration.Pose.innerPortrait, .tabletop, .outerPortrait, .innerLandscape, @@ -167,10 +175,7 @@ struct MainTabsTestsDuo { ] { try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) #expect(AdaptiveHistoryTestSupport.presentedController(from: root) === editor) - #expect(navigation.locations.selectedRegion == .california) - #expect(navigation.locations.calendar.displayedMonth == month) - #expect(navigation.settings.searchText == "appearance") - #expect(navigation.year.mode == .timeline) + #expect(navigation.selectedRegion == .california) #expect(AdaptiveHistoryTestSupport.descendants( of: editor.view, matching: UIDatePicker.self, diff --git a/Where/WhereUI/Tests/PlannedStayEditorTests.swift b/Where/WhereUI/Tests/PlannedStayEditorTests.swift index 2ebeeb96b..f5f648512 100644 --- a/Where/WhereUI/Tests/PlannedStayEditorTests.swift +++ b/Where/WhereUI/Tests/PlannedStayEditorTests.swift @@ -1,3 +1,4 @@ +import SnapshotKit import SwiftUI import TestHostSupport import Testing @@ -25,7 +26,7 @@ struct PlannedStayEditorTests { await gate.waitUntilReached() do { for pose in [ - AdaptiveHistoryPose.outerPortrait, + SnapshotConfiguration.Pose.outerPortrait, .innerPortrait, .tabletop, .outerPortrait, diff --git a/Where/WhereUI/Tests/PresenceTimelineListTests.swift b/Where/WhereUI/Tests/PresenceTimelineListTests.swift index 25238a53d..4119f686f 100644 --- a/Where/WhereUI/Tests/PresenceTimelineListTests.swift +++ b/Where/WhereUI/Tests/PresenceTimelineListTests.swift @@ -1,3 +1,5 @@ +import AccessibilitySnapshotParser +import BroadwayUI import SwiftUI import TestHostSupport import Testing @@ -6,10 +8,58 @@ import WhereCore @MainActor struct PresenceTimelineListTests { + @Test func aPendingRevealRetriesWhenAStayExtendsWithoutChangingIdentity() async throws { + let store = try TestStore() + let report = PlanningTestSupport.report(store: store) + for day in 20 ... 31 { + try await report.services.journal.addManualDay( + date: CalendarDay(year: report.selectedYear, month: 1, day: day) + .startOfDay(in: report.calendar), + regions: [.california], + audit: nil, + ) + } + await report.refresh() + let before = try PresenceTimeline.stints( + from: #require(report.report), + calendar: report.calendar, + ) + let original = try #require(before.first) + let history = HistorySelectionModel() + history.select( + month: CalendarDay(year: report.selectedYear, month: 2, day: 1), + dateCalendar: report.calendar, + ) + let host = UIHostingController(rootView: PresenceTimelineList( + report: report, + presentation: .linked, + history: history, + ).whereBroadwayRoot().bScrollRestorationScope()) + try await show(host) { _ in + #expect(history.pendingRequest(for: .timeline) != nil) + for day in 1 ... 3 { + try await report.services.journal.addManualDay( + date: CalendarDay(year: report.selectedYear, month: 2, day: day) + .startOfDay(in: report.calendar), + regions: [.california], + audit: nil, + ) + } + await report.refresh() + let updatedReport = try #require(report.report) + let extended = try #require(PresenceTimeline.stints( + from: updatedReport, + calendar: report.calendar, + ).first) + #expect(extended.id == original.id) + try await waitForAsync { history.pendingRequest(for: .timeline) == nil } + } + } + @Test func selectingTheSameMonthAgainRevealsItAfterIndependentBrowsing() async throws { let world = try await WhereFlyoverWorld.build() let report = try #require(world.report.report) - let history = HistoryNavigationModel() + let history = HistorySelectionModel() let month = CalendarDay(year: report.year, month: 5, day: 1) let end = CalendarDay(year: report.year, month: 5, day: 31) let stints = PresenceTimeline.stints(from: report, calendar: world.report.calendar) @@ -29,20 +79,62 @@ struct PresenceTimelineListTests { }) let browsed = try #require(displayedStints.last) #expect(expected.id != browsed.id) + let expectedLabel = WhereFormat.timelineRowAccessibility( + region: expected.region.localizedName, + range: DateRangeFormatting + .abbreviated( + start: expected.start, + end: expected.end, + calendar: world.report.calendar, + ), + days: expected.dayCount, + ) + let browsedLabel = WhereFormat.timelineRowAccessibility( + region: browsed.region.localizedName, + range: DateRangeFormatting + .abbreviated( + start: browsed.start, + end: browsed.end, + calendar: world.report.calendar, + ), + days: browsed.dayCount, + ) + let capture = ScrollPositionCapture() let host = UIHostingController(rootView: PresenceTimelineList( report: world.report, presentation: .linked, history: history, - ).whereBroadwayRoot()) + ).overlay { + ScrollPositionProbe( + PresenceTimelineList.self, + role: PresenceTimelineList.ScrollRole.timeline, + context: report.year, + idType: HistorySelectionModel.Item.self, + capture: capture, + ) + }.whereBroadwayRoot().bScrollRestorationScope()) try await show(host) { _ in for _ in 0 ..< 4 { history.select(month: month, dateCalendar: world.report.calendar) - try await waitForAsync { history.timeline.visibleItem == .recorded(expected.id) } - #expect(history.timeline.requestedMonth == nil) + try await waitForAsync { + capture.binding?.wrappedValue + .viewID(type: HistorySelectionModel.Item.self) == .recorded(expected.id) + && HostedAccessibility.isVisible(expectedLabel, in: host.view) + } + #expect(history.pendingRequest(for: .timeline) == nil) let selectedInterval = history.selection - history.timeline.recordVisibleItems([.recorded(browsed.id)], in: report.year) - #expect(history.timeline.visibleItem == .recorded(browsed.id)) + capture.binding?.wrappedValue.scrollTo( + id: HistorySelectionModel.Item.recorded(browsed.id), + anchor: .top, + ) + try await waitForAsync { + capture.binding?.wrappedValue + .viewID(type: HistorySelectionModel.Item.self) == .recorded(browsed.id) + && HostedAccessibility.isVisible(browsedLabel, in: host.view) + } + #expect(capture.binding?.wrappedValue + .viewID(type: HistorySelectionModel.Item.self) == .recorded(browsed.id)) #expect(history.selection == selectedInterval) } } diff --git a/Where/WhereUI/Tests/ScrollPositionTestSupport.swift b/Where/WhereUI/Tests/ScrollPositionTestSupport.swift new file mode 100644 index 000000000..b9f3b789c --- /dev/null +++ b/Where/WhereUI/Tests/ScrollPositionTestSupport.swift @@ -0,0 +1,29 @@ +import BroadwayUI +import SwiftUI + +/// Reads the production restoration entry without creating a second scroll view. +@MainActor +final class ScrollPositionCapture { + var binding: Binding? +} + +struct ScrollPositionProbe: View { + let capture: ScrollPositionCapture + @BScrollPosition private var position: ScrollPosition + + init( + _ view: (some View).Type, + role: some Hashable, + context: some Hashable, + idType: ID.Type, + capture: ScrollPositionCapture, + ) { + self.capture = capture + _position = BScrollPosition(view, role: role, context: context, idType: idType) + } + + var body: some View { + Color.clear.frame(width: 0, height: 0) + .onAppear { capture.binding = $position } + } +} diff --git a/Where/WhereUI/Tests/Support/CalendarPresentationFixture.swift b/Where/WhereUI/Tests/Support/CalendarPresentationFixture.swift new file mode 100644 index 000000000..94c92e4ae --- /dev/null +++ b/Where/WhereUI/Tests/Support/CalendarPresentationFixture.swift @@ -0,0 +1,45 @@ +import BroadwayUI +import Observation +import RegionKit +import SwiftUI +import WhereCore +@testable import WhereUI + +@MainActor @Observable +final class CalendarPresentationFixture { + var showsTabletop = false + var region: Region? +} + +/// Separate layout branches recreate the production calendar under one retained scope. +struct CalendarPresentationHarness: View { + let fixture: CalendarPresentationFixture + let report: YearReportModel + let history: HistorySelectionModel + + var body: some View { + NavigationStack { + Group { + if fixture.showsTabletop { + CalendarContentView( + focusedRegion: fixture.region, + report: report, + history: history, + presentation: .month, + persistenceKey: fixture.region.map(CalendarZoomKey.region), + ) + } else { + CalendarContentView( + focusedRegion: fixture.region, + report: report, + history: history, + persistenceKey: fixture.region.map(CalendarZoomKey.region), + ) + } + } + .id(fixture.region) + } + .whereBroadwayRoot() + .bScrollRestorationScope() + } +} diff --git a/Where/WhereUI/Tests/Support/HostedAccessibility.swift b/Where/WhereUI/Tests/Support/HostedAccessibility.swift new file mode 100644 index 000000000..0cc3acab4 --- /dev/null +++ b/Where/WhereUI/Tests/Support/HostedAccessibility.swift @@ -0,0 +1,31 @@ +import AccessibilitySnapshotParser +import UIKit + +@MainActor +enum HostedAccessibility { + static func objects(in root: NSObject) -> [NSObject] { + var visited: Set = [] + @MainActor func visit(_ object: NSObject) -> [NSObject] { + guard visited.insert(ObjectIdentifier(object)).inserted else { return [] } + var result = [object] + if let view = object as? UIView { result += view.subviews.flatMap(visit) } + let count = object.accessibilityElementCount() + if count > 0, count < 10000 { + for index in 0 ..< count { + if let child = object + .accessibilityElement(at: index) as? NSObject { result += visit(child) } + } + } + return result + } + return visit(root) + } + + static func isVisible(_ label: String, in view: UIView) -> Bool { + let viewport = view.convert(view.bounds, to: nil) + return objects(in: view) + .contains { + $0.accessibilityLabel == label && $0.accessibilityFrame.intersects(viewport) + } + } +} From b52d45f58a241c30216b15107536cee86155978f Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Wed, 11 Nov 2026 17:40:25 -0500 Subject: [PATCH 11/18] Pin the lab forecast to its initial bottom alignment --- .../adaptiveHistoryLab.outerPortrait_iPad.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png index 87fc3334d..0e7703ac8 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:474d67e94b6fd33ad1a9562de96ffc93d21a24912ca97abd3af15d52dc082941 -size 1218527 +oid sha256:28ada323688f575febe9528d4292ad5f305b3544260c7d822014ba0a0ef831a0 +size 1456189 From dc5d7b08f268b3982af155867b0accf7ba125458 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Wed, 11 Nov 2026 17:50:47 -0500 Subject: [PATCH 12/18] Keep linked timeline padding inside its first scroll target --- .../Sources/Primary/PresenceTimelineList.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift b/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift index 07e4a7140..c684183b4 100644 --- a/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift +++ b/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift @@ -146,7 +146,10 @@ struct PresenceTimelineList: View { } } .padding(.horizontal, stylesheet.spacing.xxLarge) - .padding(.top, pinsOverview ? 0 : stylesheet.spacing.large) + .padding( + .top, + pinsOverview || presentation == .linked ? 0 : stylesheet.spacing.large, + ) .padding(.bottom, stylesheet.spacing.large) } .scrollPosition($position) @@ -260,6 +263,12 @@ struct PresenceTimelineList: View { } } .fixedSize(horizontal: false, vertical: presentation == .excerpt) + // Include the content's leading gap in its first semantic target. + .padding( + .top, + presentation == .linked && item.id == recordedItems.first?.id + ? stylesheet.spacing.large : 0, + ) .id(HistorySelectionModel.Item(item)) } ForEach(plannedItems) { item in @@ -294,6 +303,11 @@ struct PresenceTimelineList: View { edit(item) } } + .padding( + .top, + presentation == .linked && recordedItems.isEmpty + && item.id == plannedItems.first?.id ? stylesheet.spacing.large : 0, + ) .id(HistorySelectionModel.Item.planned(item.id)) } else { row From ebefbbaa882054698ac8b23a55fa4b8194e91bce Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 12 Nov 2026 15:42:29 -0500 Subject: [PATCH 13/18] Carry pane and presence names through Duo consumers and lab --- Project.swift | 2 +- Where/WhereUI/AGENTS.md | 2 +- Where/WhereUI/README.md | 6 ++-- .../AdaptiveHistoryLabViewSnapshotTests.swift | 10 ------- .../DuoLayoutLabViewSnapshotTests.swift | 10 +++++++ .../LocationsViewSnapshotTests.swift | 6 ++-- .../SnapshotTests/YearViewSnapshotTests.swift | 6 ++-- .../duoLayoutLab.outerPortrait_iPad.png} | 0 .../duoLayoutLab.tabletop_iPad.png} | 0 .../Developer/DeveloperDestination.swift | 2 +- .../Sources/Developer/DeveloperOverlay.swift | 2 +- .../Sources/Developer/DeveloperTool.swift | 6 ++-- .../Sources/Developer/DeveloperToolView.swift | 6 ++-- ...LabModel.swift => DuoLayoutLabModel.swift} | 2 +- ...ryLabView.swift => DuoLayoutLabView.swift} | 22 +++++++------- ...e.swift => DuoLayoutSnapshotFixture.swift} | 20 ++++++------- .../Flyover/WhereFlyoverCatalog.swift | 2 +- Where/WhereUI/Sources/MainTabs.swift | 10 +++---- .../Sources/Primary/CalendarContentView.swift | 8 ++--- .../Primary/LocationWelcomeOverlay.swift | 2 +- .../Sources/Primary/LocationsCardsView.swift | 2 +- .../Sources/Primary/LocationsView.swift | 22 +++++++------- ...ls.swift => MonthNavigationControls.swift} | 10 +++---- .../Primary/PresenceTimelineList.swift | 28 ++++++++--------- .../Sources/Settings/SettingsView.swift | 12 ++++---- Where/WhereUI/Sources/Year/YearView.swift | 24 +++++++-------- .../Tests/CalendarContentViewTests.swift | 26 ++++++++-------- .../Tests/DeveloperDestinationTests.swift | 2 +- Where/WhereUI/Tests/LocationsViewTests.swift | 24 +++++++-------- Where/WhereUI/Tests/MainTabsTests.swift | 30 +++++++++---------- .../Tests/PlannedStayEditorTests.swift | 6 ++-- .../Tests/PresenceTimelineListTests.swift | 14 ++++----- ...re.swift => AdaptivePaneLoadFixture.swift} | 2 +- .../Support/CalendarPresentationFixture.swift | 2 +- 34 files changed, 164 insertions(+), 164 deletions(-) delete mode 100644 Where/WhereUI/SnapshotTests/AdaptiveHistoryLabViewSnapshotTests.swift create mode 100644 Where/WhereUI/SnapshotTests/DuoLayoutLabViewSnapshotTests.swift rename Where/WhereUI/SnapshotTests/__Snapshots__/{AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png => DuoLayoutLabViewSnapshotTests/duoLayoutLab.outerPortrait_iPad.png} (100%) rename Where/WhereUI/SnapshotTests/__Snapshots__/{AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png => DuoLayoutLabViewSnapshotTests/duoLayoutLab.tabletop_iPad.png} (100%) rename Where/WhereUI/Sources/Developer/DuoLayout/{AdaptiveHistoryLabModel.swift => DuoLayoutLabModel.swift} (95%) rename Where/WhereUI/Sources/Developer/DuoLayout/{AdaptiveHistoryLabView.swift => DuoLayoutLabView.swift} (88%) rename Where/WhereUI/Sources/Developer/DuoLayout/{AdaptiveHistorySnapshotFixture.swift => DuoLayoutSnapshotFixture.swift} (88%) rename Where/WhereUI/Sources/Primary/{HistoryMonthControls.swift => MonthNavigationControls.swift} (94%) rename Where/WhereUI/Tests/Support/{AdaptiveHistoryLoadFixture.swift => AdaptivePaneLoadFixture.swift} (97%) diff --git a/Project.swift b/Project.swift index 4bb1a1ce0..03769d3d8 100644 --- a/Project.swift +++ b/Project.swift @@ -772,7 +772,7 @@ let project = Project( productDependency: "WhereUI", sources: [ "Where/WhereUI/SnapshotTests/**", - "Where/WhereUI/Tests/Support/AdaptiveHistoryLoadFixture.swift", + "Where/WhereUI/Tests/Support/AdaptivePaneLoadFixture.swift", "Where/WhereUI/Tests/Support/TestStore.swift", ], extraPackageProducts: ["SnapshotKitTesting"], diff --git a/Where/WhereUI/AGENTS.md b/Where/WhereUI/AGENTS.md index d06601b56..2f5184422 100644 --- a/Where/WhereUI/AGENTS.md +++ b/Where/WhereUI/AGENTS.md @@ -298,7 +298,7 @@ Hosted tests that traverse SwiftUI accessibility must link and import `AccessibilitySnapshotParser` so a fresh simulator initializes its accessibility runtime. Guard: `CalendarContentViewTests`. -`Tests/Support/TestStore.swift` and `AdaptiveHistoryLoadFixture.swift` are shared by the unit and image bundles through `Project.swift`. +`Tests/Support/TestStore.swift` and `AdaptivePaneLoadFixture.swift` are shared by the unit and image bundles through `Project.swift`. Keep support fixtures outside `SnapshotTests/`, whose Swift files each declare one matching suite. Use the store's read gates and failures for adaptive loading snapshots. Keep fixture setup and completion independent for each configuration. `WhereStylesheetTests` pins every token default and trait-aware derivation. diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index 9f85ecd43..a79656601 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -96,13 +96,13 @@ Settings keeps its native search field visible in the navigation drawer across c Narrow windows and accessibility text sizes use one pane. The compact calendar/timeline choice remains stored while both panes are visible. Theme choices stack when the available pane cannot fit two readable card specimens. -`AdaptiveHistoryGeometry` reads independent edge insets and the system's reserved regions. -`AdaptiveHistoryArrangement` combines that geometry with the stylesheet's pane limits. +`AdaptivePaneGeometry` reads independent edge insets and the system's reserved regions. +`AdaptivePaneArrangement` combines that geometry with the stylesheet's pane limits. An active horizontal division places the selected month above the hinge and browsing controls below it. A vertical division retains native browse/detail navigation. Native containers own system-bar placement. The adapter uses container geometry, without device-name or hinge-angle checks. -`HistorySelectionModel` links explicit month and stay selections through typed day intervals. +`PresenceSelectionModel` links explicit month and stay selections through typed day intervals. It highlights the interval and reveals it in the companion pane. Scroll observations only retain local anchors. Each month reveal has a typed request identity. Consuming it preserves that identity, so repeated selections work without an observer feedback loop. Planning sheets sit above the changing containers, with one shared presentation owner per destination. diff --git a/Where/WhereUI/SnapshotTests/AdaptiveHistoryLabViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/AdaptiveHistoryLabViewSnapshotTests.swift deleted file mode 100644 index 41199d6cf..000000000 --- a/Where/WhereUI/SnapshotTests/AdaptiveHistoryLabViewSnapshotTests.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SnapshotKitTesting -import Testing -@testable import WhereUI - -@MainActor -struct AdaptiveHistoryLabViewSnapshotTests { - @Test func adaptiveHistoryLab() async { - await assertSnapshots(of: AdaptiveHistoryLabView.self) - } -} diff --git a/Where/WhereUI/SnapshotTests/DuoLayoutLabViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/DuoLayoutLabViewSnapshotTests.swift new file mode 100644 index 000000000..8147d4988 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/DuoLayoutLabViewSnapshotTests.swift @@ -0,0 +1,10 @@ +import SnapshotKitTesting +import Testing +@testable import WhereUI + +@MainActor +struct DuoLayoutLabViewSnapshotTests { + @Test func duoLayoutLab() async { + await assertSnapshots(of: DuoLayoutLabView.self) + } +} diff --git a/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift index a92f6b122..715c3c54f 100644 --- a/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift @@ -10,12 +10,12 @@ struct LocationsViewSnapshotTests { } @Test func adaptiveLoadStates() async throws { - for state in AdaptiveHistoryLoadFixture.State.allCases { + for state in AdaptivePaneLoadFixture.State.allCases { for pose in [SnapshotConfiguration.Pose.innerPortrait, .tabletop] { - let fixture = try await AdaptiveHistoryLoadFixture(state: state) + let fixture = try await AdaptivePaneLoadFixture(state: state) await assertSnapshots( of: LocationsView(report: fixture.report) - .adaptiveHistoryReservedRegions(pose.regions) + .adaptivePaneReservedRegions(pose.regions) .whereBroadwayRoot(), named: "Adaptive-" + state.rawValue + "-" + pose.rawValue, configurations: [SnapshotConfiguration(device: pose.frame)], diff --git a/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift index 4edd176ed..be7a6ac89 100644 --- a/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift @@ -10,12 +10,12 @@ struct YearViewSnapshotTests { } @Test func adaptiveLoadStates() async throws { - for state in AdaptiveHistoryLoadFixture.State.allCases { + for state in AdaptivePaneLoadFixture.State.allCases { for pose in [SnapshotConfiguration.Pose.innerPortrait, .tabletop] { - let fixture = try await AdaptiveHistoryLoadFixture(state: state) + let fixture = try await AdaptivePaneLoadFixture(state: state) await assertSnapshots( of: YearView(report: fixture.report) - .adaptiveHistoryReservedRegions(pose.regions) + .adaptivePaneReservedRegions(pose.regions) .whereBroadwayRoot(), named: "Adaptive-" + state.rawValue + "-" + pose.rawValue, configurations: [SnapshotConfiguration(device: pose.frame)], diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DuoLayoutLabViewSnapshotTests/duoLayoutLab.outerPortrait_iPad.png similarity index 100% rename from Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.outerPortrait_iPad.png rename to Where/WhereUI/SnapshotTests/__Snapshots__/DuoLayoutLabViewSnapshotTests/duoLayoutLab.outerPortrait_iPad.png diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DuoLayoutLabViewSnapshotTests/duoLayoutLab.tabletop_iPad.png similarity index 100% rename from Where/WhereUI/SnapshotTests/__Snapshots__/AdaptiveHistoryLabViewSnapshotTests/adaptiveHistoryLab.tabletop_iPad.png rename to Where/WhereUI/SnapshotTests/__Snapshots__/DuoLayoutLabViewSnapshotTests/duoLayoutLab.tabletop_iPad.png diff --git a/Where/WhereUI/Sources/Developer/DeveloperDestination.swift b/Where/WhereUI/Sources/Developer/DeveloperDestination.swift index fef508381..109961976 100644 --- a/Where/WhereUI/Sources/Developer/DeveloperDestination.swift +++ b/Where/WhereUI/Sources/Developer/DeveloperDestination.swift @@ -37,7 +37,7 @@ .tool(.logs), .tool(.openSpans), .flyover, - .tool(.adaptiveHistory), + .tool(.duoLayoutLab), .tool(.regionMap), .tool(.crashTesting), ] diff --git a/Where/WhereUI/Sources/Developer/DeveloperOverlay.swift b/Where/WhereUI/Sources/Developer/DeveloperOverlay.swift index f75b7888b..69bbf330e 100644 --- a/Where/WhereUI/Sources/Developer/DeveloperOverlay.swift +++ b/Where/WhereUI/Sources/Developer/DeveloperOverlay.swift @@ -58,7 +58,7 @@ } var body: some View { - AdaptiveHistoryGeometryReader { geometry in + AdaptivePaneGeometryReader { geometry in let leadingIsLeft = layoutDirection == .leftToRight let preferred = CGPoint( x: model.corner.isLeading == leadingIsLeft ? geometry.contentBounds diff --git a/Where/WhereUI/Sources/Developer/DeveloperTool.swift b/Where/WhereUI/Sources/Developer/DeveloperTool.swift index 94be065fa..6b1808d54 100644 --- a/Where/WhereUI/Sources/Developer/DeveloperTool.swift +++ b/Where/WhereUI/Sources/Developer/DeveloperTool.swift @@ -12,7 +12,7 @@ case logs case openSpans case regionMap - case adaptiveHistory + case duoLayoutLab var id: Self { self @@ -28,7 +28,7 @@ String(localized: .developerOpenSpansLink) case .regionMap: String(localized: .developerRegionMapLink) - case .adaptiveHistory: + case .duoLayoutLab: String(localized: .developerDuoTitle) } } @@ -39,7 +39,7 @@ case .logs: .ladybug case .openSpans: .timer case .regionMap: .map - case .adaptiveHistory: .rectangleSplit2x1 + case .duoLayoutLab: .rectangleSplit2x1 } } } diff --git a/Where/WhereUI/Sources/Developer/DeveloperToolView.swift b/Where/WhereUI/Sources/Developer/DeveloperToolView.swift index c24bcbd65..c456076df 100644 --- a/Where/WhereUI/Sources/Developer/DeveloperToolView.swift +++ b/Where/WhereUI/Sources/Developer/DeveloperToolView.swift @@ -20,7 +20,7 @@ import SFSafeSymbols @Environment(WhereModel.self) private var model: WhereModel? var body: some View { - if tool == .adaptiveHistory { + if tool == .duoLayoutLab { toolContent } else { NavigationStack { toolContent } @@ -71,8 +71,8 @@ import SFSafeSymbols case .regionMap: RegionMapView() - case .adaptiveHistory: - AdaptiveHistoryLabView() + case .duoLayoutLab: + DuoLayoutLabView() } } } diff --git a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabModel.swift b/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutLabModel.swift similarity index 95% rename from Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabModel.swift rename to Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutLabModel.swift index de74837ff..9d8a3feb4 100644 --- a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabModel.swift +++ b/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutLabModel.swift @@ -3,7 +3,7 @@ /// Builds one separate demo world and retains its scene through every synthetic pose. @MainActor @Observable - final class AdaptiveHistoryLabModel { + final class DuoLayoutLabModel { enum State { case idle case loading diff --git a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift b/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutLabView.swift similarity index 88% rename from Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift rename to Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutLabView.swift index 9273df541..83d8571b0 100644 --- a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistoryLabView.swift +++ b/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutLabView.swift @@ -4,13 +4,13 @@ import SwiftUI /// A DEBUG-only mounted production scene. Pose controls change geometry, never its models. - struct AdaptiveHistoryLabView: View { - @State private var model: AdaptiveHistoryLabModel + struct DuoLayoutLabView: View { + @State private var model: DuoLayoutLabModel @State private var pose: SnapshotConfiguration.Pose private let initialRegion: Region? init( - model: AdaptiveHistoryLabModel = AdaptiveHistoryLabModel(), + model: DuoLayoutLabModel = DuoLayoutLabModel(), pose: SnapshotConfiguration.Pose = .outerPortrait, initialRegion: Region? = nil, ) { @@ -81,7 +81,7 @@ )) .frame(width: pose.size.width, height: pose.size.height) .background(.background) - .adaptiveHistoryReservedRegions(pose.regions) + .adaptivePaneReservedRegions(pose.regions) .whereBroadwayRoot() .snapshotTraits(SnapshotConfiguration(device: pose.frame)) .frame(width: pose.size.width, height: pose.size.height) @@ -92,10 +92,10 @@ } } - extension AdaptiveHistoryLabView: SnapshotProviding { + extension DuoLayoutLabView: SnapshotProviding { static var snapshots: [SnapshotCase] { [SnapshotConfiguration.Pose.outerPortrait, .tabletop].map { pose in - let fixture = AdaptiveHistorySnapshotFixture(tab: .locations, selection: .region) + let fixture = DuoLayoutSnapshotFixture(tab: .locations, selection: .region) return whereSnapshot( name: pose.rawValue, configurations: [SnapshotConfiguration(device: .viewport( @@ -110,7 +110,7 @@ ))], onReadyToSnapshot: { await fixture.prepare() }, ) { - AdaptiveHistoryLabView( + DuoLayoutLabView( model: fixture.model, pose: pose, initialRegion: .california, @@ -121,15 +121,15 @@ } } - #Preview { AdaptiveHistoryLabView.snapshotPreviews } + #Preview { DuoLayoutLabView.snapshotPreviews } - extension AdaptiveHistoryLabView: WhereFlyoverProviding { + extension DuoLayoutLabView: WhereFlyoverProviding { static let flyoverData = WhereFlyoverData.hosted( - AdaptiveHistoryLabView.self, + DuoLayoutLabView.self, title: "Duo Layout Lab", navigationContainer: .none, ) { _ in - AdaptiveHistoryLabView() + DuoLayoutLabView() } } #endif diff --git a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistorySnapshotFixture.swift b/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutSnapshotFixture.swift similarity index 88% rename from Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistorySnapshotFixture.swift rename to Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutSnapshotFixture.swift index 6f824343a..c68561f36 100644 --- a/Where/WhereUI/Sources/Developer/DuoLayout/AdaptiveHistorySnapshotFixture.swift +++ b/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutSnapshotFixture.swift @@ -5,10 +5,10 @@ /// Each capture configuration gets its own demo world, navigation, and artwork readiness. @MainActor - final class AdaptiveHistorySnapshotFixture { + final class DuoLayoutSnapshotFixture { static let browseColumn = SnapshotConfiguration.Frame.fullContent( name: "browseColumn", - width: WhereStylesheet.default.adaptiveHistory.minimumControlsWidth, + width: WhereStylesheet.default.adaptivePane.minimumControlsWidth, minimumHeight: 669, layoutTraits: .init( horizontalSizeClass: .regular, @@ -18,7 +18,7 @@ ) static let historyColumn = SnapshotConfiguration.Frame.fullContent( name: "historyColumn", - width: WhereStylesheet.default.adaptiveHistory.minimumHistoryWidth, + width: WhereStylesheet.default.adaptivePane.minimumDetailWidth, minimumHeight: 669, layoutTraits: .init( horizontalSizeClass: .regular, @@ -35,10 +35,10 @@ case settings } - let model = AdaptiveHistoryLabModel() + let model = DuoLayoutLabModel() let artwork = RegionOutlinePathCache() let tab: MainTabSelection.TabID - let history = HistorySelectionModel() + let history = PresenceSelectionModel() let selection: Selection private var didSelect = false @@ -90,16 +90,16 @@ } #Preview { - AdaptiveHistorySnapshotScene( - fixture: AdaptiveHistorySnapshotFixture(tab: .locations, selection: .region), + DuoLayoutSnapshotScene( + fixture: DuoLayoutSnapshotFixture(tab: .locations, selection: .region), pose: .innerPortrait, ) .whereBroadwayRoot() .snapshotTraits(SnapshotConfiguration(device: .duoInnerPortrait)) } - struct AdaptiveHistorySnapshotScene: View { - let fixture: AdaptiveHistorySnapshotFixture + struct DuoLayoutSnapshotScene: View { + let fixture: DuoLayoutSnapshotFixture let pose: SnapshotConfiguration.Pose var body: some View { @@ -129,7 +129,7 @@ ) } } - .adaptiveHistoryReservedRegions(pose.regions) + .adaptivePaneReservedRegions(pose.regions) .task { await fixture.prepare() } } } diff --git a/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverCatalog.swift b/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverCatalog.swift index f527d71a2..39cc54fb4 100644 --- a/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverCatalog.swift +++ b/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverCatalog.swift @@ -120,7 +120,7 @@ WhereFlyoverLogView.flyoverData, OpenSpansView.flyoverData, RegionMapView.flyoverData, - AdaptiveHistoryLabView.flyoverData, + DuoLayoutLabView.flyoverData, ] } diff --git a/Where/WhereUI/Sources/MainTabs.swift b/Where/WhereUI/Sources/MainTabs.swift index 6d182bc87..e3a1cf378 100644 --- a/Where/WhereUI/Sources/MainTabs.swift +++ b/Where/WhereUI/Sources/MainTabs.swift @@ -36,7 +36,7 @@ struct MainTabs: View { @State private var selection = MainTabSelection() private var initialRegion: Region? private var initialSettingsRoute: SettingsRoute? - private var initialHistory: HistorySelectionModel? + private var initialHistory: PresenceSelectionModel? @Environment(UpdateModel.self) private var updates: UpdateModel? @Environment(\.isInDemoMode) private var isInDemoMode @Environment(\.scenePhase) private var scenePhase @@ -227,7 +227,7 @@ struct MainTabs: View { tab: TabID = .locations, region: Region? = nil, settingsRoute: SettingsRoute? = nil, - history: HistorySelectionModel? = nil, + history: PresenceSelectionModel? = nil, ) { self.init( session: world.session, @@ -412,7 +412,7 @@ struct MainTabs: View { tab: MainTabSelection.TabID, pose: SnapshotConfiguration.Pose, configuration: SnapshotConfiguration, - selection: AdaptiveHistorySnapshotFixture.Selection, + selection: DuoLayoutSnapshotFixture.Selection, ) -> SnapshotCase { let tabName = switch tab { case .locations: "Locations" @@ -420,7 +420,7 @@ struct MainTabs: View { case .year: "Year" case .settings: "Settings" } - let fixture = AdaptiveHistorySnapshotFixture(tab: tab, selection: selection) + let fixture = DuoLayoutSnapshotFixture(tab: tab, selection: selection) let selectionName = switch selection { case .none: "" case .region, .stay, .settings: "-Selected" @@ -437,7 +437,7 @@ struct MainTabs: View { }, ) { - AdaptiveHistorySnapshotScene(fixture: fixture, pose: pose) + DuoLayoutSnapshotScene(fixture: fixture, pose: pose) } } diff --git a/Where/WhereUI/Sources/Primary/CalendarContentView.swift b/Where/WhereUI/Sources/Primary/CalendarContentView.swift index bdb353c18..5b1e6db3d 100644 --- a/Where/WhereUI/Sources/Primary/CalendarContentView.swift +++ b/Where/WhereUI/Sources/Primary/CalendarContentView.swift @@ -13,7 +13,7 @@ import SwiftUI /// it — region-focused, with a title — as the zoom destination of a tapped card. struct CalendarContentView: View { enum Presentation: Equatable { case year, month } - let history: HistorySelectionModel? + let history: PresenceSelectionModel? let displayMode: Presentation enum ScrollRole: Hashable { case standalone, locations, year } struct ScrollContext: Hashable { let region: Region?; let year: Int } @@ -67,7 +67,7 @@ struct CalendarContentView: View { init( focusedRegion: Region? = nil, report: YearReportModel, - history: HistorySelectionModel? = nil, + history: PresenceSelectionModel? = nil, presentationState: CalendarPresentationState? = nil, presentation: Presentation = .year, initialMode: CalendarPresentationState.Mode = .detail, @@ -774,7 +774,7 @@ private struct MonthGridView: View { compact: compact, isAvailable: isAvailable, ) - .historySelection(selectedInterval?.contains(CalendarDay( + .presenceSelection(selectedInterval?.contains(CalendarDay( from: month.days[index].date, in: dateCalendar, )) == true) @@ -1175,7 +1175,7 @@ private struct DayCell: View { static var snapshots: [SnapshotCase] { whereSnapshot( name: "HistoryColumn", - configurations: [SnapshotConfiguration(device: AdaptiveHistorySnapshotFixture + configurations: [SnapshotConfiguration(device: DuoLayoutSnapshotFixture .historyColumn)], ) { NavigationStack { diff --git a/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift b/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift index 8b4912068..590535d4c 100644 --- a/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift +++ b/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift @@ -22,7 +22,7 @@ struct LocationWelcomeOverlay: View { .transition(.opacity) .zIndex(0) - AdaptiveHistoryGeometryReader { geometry in + AdaptivePaneGeometryReader { geometry in let area = geometry.unobstructedBounds(near: CGPoint( x: geometry.contentBounds.midX, y: geometry.contentBounds.maxY, diff --git a/Where/WhereUI/Sources/Primary/LocationsCardsView.swift b/Where/WhereUI/Sources/Primary/LocationsCardsView.swift index 00c46dd2d..a466e5e37 100644 --- a/Where/WhereUI/Sources/Primary/LocationsCardsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsCardsView.swift @@ -123,7 +123,7 @@ struct LocationsCardsView: View { ) .locationCardRankingRegion(item.region) .id(item.region) - .historySelection( + .presenceSelection( navigation.selectedRegion == item.region, cornerRadius: stylesheet.card[cardVariant].cornerRadius, ) diff --git a/Where/WhereUI/Sources/Primary/LocationsView.swift b/Where/WhereUI/Sources/Primary/LocationsView.swift index 8288a236d..faac00e27 100644 --- a/Where/WhereUI/Sources/Primary/LocationsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsView.swift @@ -39,7 +39,7 @@ struct LocationsView: View { let report: YearReportModel var openElsewhere: (() -> Void)? @State private var navigation: LocationsView.Navigation - @State private var history = HistorySelectionModel() + @State private var history = PresenceSelectionModel() @State private var calendarPresentation: CalendarPresentationState @State private var cardPresentation: LocationCardsPresentationModel @State private var tilt = TiltProvider() @@ -69,15 +69,15 @@ struct LocationsView: View { var body: some View { @Bindable var navigation = navigation - AdaptiveHistoryContainer { arrangement in + AdaptivePaneContainer { arrangement in if let panes = tabletopPanes(for: arrangement) { - TabletopHistoryLayout(panes: panes) { + TabletopPaneLayout(panes: panes) { calendar(presentation: .month, usesZoomTransition: false) } controls: { NavigationStack { browseContent .safeAreaInset(edge: .top, spacing: 0) { - HistoryMonthControls( + MonthNavigationControls( report: report, history: history, region: navigation.selectedRegion, @@ -101,8 +101,8 @@ struct LocationsView: View { openElsewhere: openElsewhere, ) .navigationSplitViewColumnWidth( - min: stylesheet.adaptiveHistory.minimumControlsWidth, - ideal: stylesheet.adaptiveHistory.minimumControlsWidth, + min: stylesheet.adaptivePane.minimumControlsWidth, + ideal: stylesheet.adaptivePane.minimumControlsWidth, ) } detail: { calendar( @@ -110,8 +110,8 @@ struct LocationsView: View { usesZoomTransition: arrangement == .compact, ) .navigationSplitViewColumnWidth( - min: stylesheet.adaptiveHistory.minimumHistoryWidth, - ideal: stylesheet.adaptiveHistory.minimumHistoryWidth, + min: stylesheet.adaptivePane.minimumDetailWidth, + ideal: stylesheet.adaptivePane.minimumDetailWidth, ) } .navigationSplitViewStyle(.balanced) @@ -149,8 +149,8 @@ struct LocationsView: View { /// Drill-ins keep their native stack mounted while their local editors are open. /// The custom tabletop composition belongs to the top-level history surface. - private func tabletopPanes(for arrangement: AdaptiveHistoryArrangement) - -> AdaptiveHistoryArrangement.Panes? + private func tabletopPanes(for arrangement: AdaptivePaneArrangement) + -> AdaptivePaneArrangement.Panes? { guard navigation.browsePath.isEmpty, navigation.detailPath.isEmpty, case let .tabletop(panes) = arrangement else { return nil } @@ -450,7 +450,7 @@ private struct ResolveToolbarLabel: View { artworkSnapshot( name: "BrowseColumn", report: PreviewSupport.loadedYearReportModel(), - configurations: [SnapshotConfiguration(device: AdaptiveHistorySnapshotFixture + configurations: [SnapshotConfiguration(device: DuoLayoutSnapshotFixture .browseColumn)], cardVariant: .column, ) diff --git a/Where/WhereUI/Sources/Primary/HistoryMonthControls.swift b/Where/WhereUI/Sources/Primary/MonthNavigationControls.swift similarity index 94% rename from Where/WhereUI/Sources/Primary/HistoryMonthControls.swift rename to Where/WhereUI/Sources/Primary/MonthNavigationControls.swift index 58da171f3..a6383b007 100644 --- a/Where/WhereUI/Sources/Primary/HistoryMonthControls.swift +++ b/Where/WhereUI/Sources/Primary/MonthNavigationControls.swift @@ -5,13 +5,13 @@ import SwiftUI import WhereCore /// Explicit month browsing controls shared by both tabletop destinations. -struct HistoryMonthControls: View { +struct MonthNavigationControls: View { let report: YearReportModel - let history: HistorySelectionModel + let history: PresenceSelectionModel @BScrollPosition private var position: ScrollPosition init( report: YearReportModel, - history: HistorySelectionModel, + history: PresenceSelectionModel, region: Region? = nil, scrollRole: CalendarContentView.ScrollRole = .year, ) { @@ -89,9 +89,9 @@ struct HistoryMonthControls: View { #if DEBUG #Preview { - HistoryMonthControls( + MonthNavigationControls( report: PreviewSupport.loadedYearReportModel(), - history: HistorySelectionModel(), + history: PresenceSelectionModel(), ) .whereBroadwayRoot() } diff --git a/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift b/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift index c684183b4..59aa49d12 100644 --- a/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift +++ b/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift @@ -23,19 +23,19 @@ struct PresenceTimelineList: View { let presentation: Presentation private let suppliedPlanningDestination: Binding? @State private var localPlanningDestination: PlannedStaysDestination? - @BScrollPosition private var position: ScrollPosition + @BScrollPosition private var position: ScrollPosition enum ScrollRole: Hashable { case timeline } private var planningDestination: Binding { suppliedPlanningDestination ?? $localPlanningDestination } @Environment(\.stylesheet) private var stylesheet - @State private var history: HistorySelectionModel + @State private var history: PresenceSelectionModel init( report: YearReportModel, presentation: Presentation = .full, - history: HistorySelectionModel? = nil, + history: PresenceSelectionModel? = nil, planningDestination: Binding? = nil, ) { self.report = report @@ -45,9 +45,9 @@ struct PresenceTimelineList: View { Self.self, role: ScrollRole.timeline, context: { report.selectedYear }, - idType: HistorySelectionModel.Item.self, + idType: PresenceSelectionModel.Item.self, ) - _history = State(initialValue: history ?? HistorySelectionModel()) + _history = State(initialValue: history ?? PresenceSelectionModel()) } var body: some View { @@ -67,7 +67,7 @@ struct PresenceTimelineList: View { today: report.forecasts.today, ) : [] - let rowIDs = recordedItems.map(HistorySelectionModel.Item.init) + let rowIDs = recordedItems.map(PresenceSelectionModel.Item.init) + plannedItems.map { .planned($0.id) } Group { @@ -161,10 +161,10 @@ struct PresenceTimelineList: View { )) { guard !Task.isCancelled else { return } let initial = presentation == .linked ? recordedItems.last - .map(HistorySelectionModel.Item.init) : nil + .map(PresenceSelectionModel.Item.init) : nil if let initial { _position.initialize(id: initial, anchor: .top) } else { _position.initialize(edge: .bottom) } - if let target = position.viewID(type: HistorySelectionModel.Item.self), + if let target = position.viewID(type: PresenceSelectionModel.Item.self), !rowIDs.contains(target) { if let initial { _position.scrollTo(id: initial, anchor: .top) } @@ -212,7 +212,7 @@ struct PresenceTimelineList: View { let year: Int let recordedItems: [PresenceJourneyItem] let plannedItems: [PlanningTimelineItem] - let request: HistorySelectionModel.Request.ID? + let request: PresenceSelectionModel.Request.ID? } /// Both presentations share recorded and planned row rendering. @@ -245,7 +245,7 @@ struct PresenceTimelineList: View { Button { history.select(stint: stint, dateCalendar: report.calendar) } label: { - row.historySelection(isSelected( + row.presenceSelection(isSelected( start: CalendarDay(from: stint.start, in: report.calendar), end: CalendarDay(from: stint.end, in: report.calendar), region: stint.region, @@ -269,7 +269,7 @@ struct PresenceTimelineList: View { presentation == .linked && item.id == recordedItems.first?.id ? stylesheet.spacing.large : 0, ) - .id(HistorySelectionModel.Item(item)) + .id(PresenceSelectionModel.Item(item)) } ForEach(plannedItems) { item in let row = PlannedPresenceJourneyRow( @@ -288,7 +288,7 @@ struct PresenceTimelineList: View { history.select(plannedItem: item) if presentation == .full { edit(item) } } label: { - row.historySelection(isSelected( + row.presenceSelection(isSelected( start: item.start, end: item.end, region: item.region, @@ -308,7 +308,7 @@ struct PresenceTimelineList: View { presentation == .linked && recordedItems.isEmpty && item.id == plannedItems.first?.id ? stylesheet.spacing.large : 0, ) - .id(HistorySelectionModel.Item.planned(item.id)) + .id(PresenceSelectionModel.Item.planned(item.id)) } else { row } @@ -358,7 +358,7 @@ struct PresenceTimelineList: View { }, whereSnapshot( name: "BrowseColumn", - configurations: [SnapshotConfiguration(device: AdaptiveHistorySnapshotFixture + configurations: [SnapshotConfiguration(device: DuoLayoutSnapshotFixture .browseColumn)], ) { NavigationStack { diff --git a/Where/WhereUI/Sources/Settings/SettingsView.swift b/Where/WhereUI/Sources/Settings/SettingsView.swift index 0cf42565f..db96cf555 100644 --- a/Where/WhereUI/Sources/Settings/SettingsView.swift +++ b/Where/WhereUI/Sources/Settings/SettingsView.swift @@ -74,7 +74,7 @@ struct SettingsView: View { var body: some View { @Bindable var navigation = navigation - AdaptiveHistoryContainer { arrangement in + AdaptivePaneContainer { arrangement in NavigationSplitView( columnVisibility: .constant(.all), preferredCompactColumn: $navigation.compactColumn, @@ -85,8 +85,8 @@ struct SettingsView: View { navigation: navigation, ) .navigationSplitViewColumnWidth( - min: stylesheet.adaptiveHistory.minimumControlsWidth, - ideal: stylesheet.adaptiveHistory.minimumHistoryWidth, + min: stylesheet.adaptivePane.minimumControlsWidth, + ideal: stylesheet.adaptivePane.minimumDetailWidth, ) } detail: { NavigationStack(path: $navigation.path) { @@ -105,8 +105,8 @@ struct SettingsView: View { } } .navigationSplitViewColumnWidth( - min: stylesheet.adaptiveHistory.minimumHistoryWidth, - ideal: stylesheet.adaptiveHistory.minimumHistoryWidth, + min: stylesheet.adaptivePane.minimumDetailWidth, + ideal: stylesheet.adaptivePane.minimumDetailWidth, ) } .navigationSplitViewStyle(.balanced) @@ -237,7 +237,7 @@ private struct SettingsDestinationView: View { static var snapshots: [SnapshotCase] { whereSnapshot( name: "BrowseColumn", - configurations: [SnapshotConfiguration(device: AdaptiveHistorySnapshotFixture + configurations: [SnapshotConfiguration(device: DuoLayoutSnapshotFixture .browseColumn)], ) { snapshotList(report: PreviewSupport.loadedYearReportModel()) diff --git a/Where/WhereUI/Sources/Year/YearView.swift b/Where/WhereUI/Sources/Year/YearView.swift index 7f424e5b3..14e39a8ec 100644 --- a/Where/WhereUI/Sources/Year/YearView.swift +++ b/Where/WhereUI/Sources/Year/YearView.swift @@ -11,7 +11,7 @@ struct YearView: View { let report: YearReportModel @State private var mode: YearMode - @State private var history: HistorySelectionModel + @State private var history: PresenceSelectionModel @State private var planningDestination: PlannedStaysDestination? @State private var calendarPresentation: CalendarPresentationState @@ -20,11 +20,11 @@ struct YearView: View { init( report: YearReportModel, initialMode: YearMode = .calendar, - history: HistorySelectionModel? = nil, + history: PresenceSelectionModel? = nil, ) { self.report = report _mode = State(initialValue: initialMode) - _history = State(initialValue: history ?? HistorySelectionModel()) + _history = State(initialValue: history ?? PresenceSelectionModel()) _calendarPresentation = State(initialValue: CalendarPresentationState(mode: CalendarModel( report: report, persistenceKey: .year, @@ -32,7 +32,7 @@ struct YearView: View { } var body: some View { - AdaptiveHistoryContainer { arrangement in + AdaptivePaneContainer { arrangement in switch arrangement { case .compact: YearCompactContent( @@ -46,25 +46,25 @@ struct YearView: View { NavigationSplitView(columnVisibility: .constant(.all)) { timeline(presentation: .linked) .navigationSplitViewColumnWidth( - min: stylesheet.adaptiveHistory.minimumControlsWidth, - ideal: stylesheet.adaptiveHistory.minimumControlsWidth, + min: stylesheet.adaptivePane.minimumControlsWidth, + ideal: stylesheet.adaptivePane.minimumControlsWidth, ) } detail: { calendar(presentation: .year) .navigationSplitViewColumnWidth( - min: stylesheet.adaptiveHistory.minimumHistoryWidth, - ideal: stylesheet.adaptiveHistory.minimumHistoryWidth, + min: stylesheet.adaptivePane.minimumDetailWidth, + ideal: stylesheet.adaptivePane.minimumDetailWidth, ) } .navigationSplitViewStyle(.balanced) case let .tabletop(panes): - TabletopHistoryLayout(panes: panes) { + TabletopPaneLayout(panes: panes) { NavigationStack { calendar(presentation: .month) } } controls: { NavigationStack { timeline(presentation: .linked) .safeAreaInset(edge: .top, spacing: 0) { - HistoryMonthControls( + MonthNavigationControls( report: report, history: history, ) @@ -107,7 +107,7 @@ struct YearView: View { struct YearCompactView: View { let report: YearReportModel @State private var mode = YearMode.calendar - @State private var history = HistorySelectionModel() + @State private var history = PresenceSelectionModel() @State private var planningDestination: PlannedStaysDestination? @State private var calendarPresentation: CalendarPresentationState @@ -135,7 +135,7 @@ struct YearCompactView: View { private struct YearCompactContent: View { let report: YearReportModel @Binding var mode: YearMode - let history: HistorySelectionModel + let history: PresenceSelectionModel let calendarPresentation: CalendarPresentationState @Binding var planningDestination: PlannedStaysDestination? @Environment(\.stylesheet) private var stylesheet diff --git a/Where/WhereUI/Tests/CalendarContentViewTests.swift b/Where/WhereUI/Tests/CalendarContentViewTests.swift index df60f4656..9d8b71d09 100644 --- a/Where/WhereUI/Tests/CalendarContentViewTests.swift +++ b/Where/WhereUI/Tests/CalendarContentViewTests.swift @@ -103,7 +103,7 @@ struct CalendarContentViewTests { @Test func overviewAndTabletopDoNotReplaceTheForecastAnchor() async throws { let report = PreviewSupport.loadedYearReportModel() let fixture = CalendarPresentationFixture() - let history = HistorySelectionModel() + let history = PresenceSelectionModel() let host = UIHostingController(rootView: CalendarPresentationHarness( fixture: fixture, report: report, @@ -164,13 +164,13 @@ struct CalendarContentViewTests { @Test func theUnselectedLocationsCalendarKeepsItsSessionOnlyZoomMode() async throws { let report = PreviewSupport.loadedYearReportModel() - let state = AdaptiveHistoryHostState() - let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + let state = AdaptivePaneHostState() + let host = UIHostingController(rootView: AdaptivePaneHostedSurface(state: state) { LocationsView(report: report) }.bScrollRestorationScope()) host.safeAreaRegions = [] try await show(host) { controller in - try await AdaptiveHistoryTestSupport.resize( + try await AdaptivePaneTestSupport.resize( controller, state: state, to: .innerLandscape, @@ -184,13 +184,13 @@ struct CalendarContentViewTests { "Show Months", in: host.view, ) } - try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: .tabletop) + try await AdaptivePaneTestSupport.resize(controller, state: state, to: .tabletop) try await waitForCalendar("tabletop calendar") { isVisible( "July", in: host.view, expanded: true, ) } - try await AdaptiveHistoryTestSupport.resize( + try await AdaptivePaneTestSupport.resize( controller, state: state, to: .innerLandscape, @@ -209,13 +209,13 @@ struct CalendarContentViewTests { report.preferences.setCalendarOverview(false, forKey: .region(.newYork)) let navigation = LocationsView.Navigation() navigation.select(region: .california) - let state = AdaptiveHistoryHostState() - let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + let state = AdaptivePaneHostState() + let host = UIHostingController(rootView: AdaptivePaneHostedSurface(state: state) { LocationsView(report: report, navigation: navigation) }.bScrollRestorationScope()) host.safeAreaRegions = [] try await show(host) { controller in - try await AdaptiveHistoryTestSupport.resize( + try await AdaptivePaneTestSupport.resize( controller, state: state, to: .innerLandscape, @@ -281,7 +281,7 @@ struct CalendarContentViewTests { @Test func aRevealWaitsForDelayedCalendarData() async throws { let store = try TestStore() let report = PlanningTestSupport.report(store: store) - let history = HistorySelectionModel() + let history = PresenceSelectionModel() let month = CalendarDay(year: report.selectedYear, month: 2, day: 1) history.select(month: month, dateCalendar: report.calendar) await store.enableFirstSamplesGate() @@ -313,7 +313,7 @@ struct CalendarContentViewTests { @Test func repeatedLinkedSelectionExpandsOverviewAndKeepsMonthTarget() async throws { let report = PreviewSupport.loadedYearReportModel() - let history = HistorySelectionModel() + let history = PresenceSelectionModel() let month = CalendarDay(year: report.selectedYear, month: 2, day: 1) let host = UIHostingController(rootView: NavigationStack { CalendarContentView( @@ -344,7 +344,7 @@ struct CalendarContentViewTests { @Test func mountedCalendarPositionsAfterTheNewYearsMonthsArrive() async throws { let report = PreviewSupport.loadedYearReportModel() - let history = HistorySelectionModel() + let history = PresenceSelectionModel() let host = UIHostingController(rootView: CalendarContentView( report: report, history: history, @@ -403,7 +403,7 @@ struct CalendarContentViewTests { @Test func aRemountedCalendarUsesItsRenderedMonthAnchor() async throws { let report = PreviewSupport.loadedYearReportModel() - let history = HistorySelectionModel() + let history = PresenceSelectionModel() let store = BScrollRestorationStore() let first = UIHostingController(rootView: CalendarContentView( report: report, diff --git a/Where/WhereUI/Tests/DeveloperDestinationTests.swift b/Where/WhereUI/Tests/DeveloperDestinationTests.swift index 5a99c4d82..cbb351a10 100644 --- a/Where/WhereUI/Tests/DeveloperDestinationTests.swift +++ b/Where/WhereUI/Tests/DeveloperDestinationTests.swift @@ -10,7 +10,7 @@ .tool(.logs), .tool(.openSpans), .flyover, - .tool(.adaptiveHistory), + .tool(.duoLayoutLab), .tool(.regionMap), .tool(.crashTesting), ], diff --git a/Where/WhereUI/Tests/LocationsViewTests.swift b/Where/WhereUI/Tests/LocationsViewTests.swift index 85102579a..97f381b20 100644 --- a/Where/WhereUI/Tests/LocationsViewTests.swift +++ b/Where/WhereUI/Tests/LocationsViewTests.swift @@ -42,17 +42,17 @@ struct LocationsViewTestsDuo { controller.traitOverrides.horizontalSizeClass = .compact controller.view.layoutIfNeeded() try await waitForStage("initial scroll content") { - AdaptiveHistoryTestSupport.descendants( + AdaptivePaneTestSupport.descendants( of: controller.view, matching: UIScrollView.self, ).contains { $0.contentSize.height > $0.bounds.height } } - var scroll = try #require(AdaptiveHistoryTestSupport.descendants( + var scroll = try #require(AdaptivePaneTestSupport.descendants( of: controller.view, matching: UIScrollView.self, ).first { $0.contentSize.height > $0.bounds.height }) @MainActor func currentScroll() -> UIScrollView? { - AdaptiveHistoryTestSupport.descendants( + AdaptivePaneTestSupport.descendants( of: controller.view, matching: UIScrollView.self, ) @@ -92,7 +92,7 @@ struct LocationsViewTestsDuo { controller.view.frame = CGRect(x: 0, y: 0, width: 466, height: 678) controller.view.layoutIfNeeded() try await waitForStage("restored California after pane replacement") { - AdaptiveHistoryTestSupport.descendants( + AdaptivePaneTestSupport.descendants( of: controller.view, matching: UIScrollView.self, ) @@ -107,8 +107,8 @@ struct LocationsViewTestsDuo { @Test func aPushedBrowseStackRemainsMountedThroughTabletop() async throws { let world = try await WhereFlyoverWorld.build() let navigation = LocationsView.Navigation() - let state = AdaptiveHistoryHostState() - let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + let state = AdaptivePaneHostState() + let host = UIHostingController(rootView: AdaptivePaneHostedSurface(state: state) { LocationsView(report: world.report, navigation: navigation) .environment(world.session).environment(world.model) }.bScrollRestorationScope()) @@ -118,7 +118,7 @@ struct LocationsViewTestsDuo { } try await show(host) { controller in controller.view.window?.layer.speed = 1 - try await AdaptiveHistoryTestSupport.resize( + try await AdaptivePaneTestSupport.resize( controller, state: state, to: .outerPortrait, @@ -142,7 +142,7 @@ struct LocationsViewTestsDuo { .first { $0.viewControllers.count > 1 }) let pushed = try #require(stack.topViewController) for pose in [SnapshotConfiguration.Pose.innerPortrait, .tabletop, .outerPortrait] { - try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) + try await AdaptivePaneTestSupport.resize(controller, state: state, to: pose) #expect(navigation.browsePath.count == 1) #expect(controllers(controller).contains { $0 === pushed }) } @@ -173,8 +173,8 @@ struct LocationsViewTestsDuo { await store.awaitFirstSamplesCall() let navigation = LocationsView.Navigation() navigation.select(region: .california) - let state = AdaptiveHistoryHostState() - let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + let state = AdaptivePaneHostState() + let host = UIHostingController(rootView: AdaptivePaneHostedSurface(state: state) { LocationsView(report: report, navigation: navigation) }) host.safeAreaRegions = [] @@ -187,14 +187,14 @@ struct LocationsViewTestsDuo { .tabletop, .outerPortrait, ] { - try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) + try await AdaptivePaneTestSupport.resize(controller, state: state, to: pose) #expect(await store.sampleReadCount == readsBeforeFolding) #expect(report.loadState == .loading) } await report.select(year: 2026) let readsAfterYearChange = await store.sampleReadCount for pose in [SnapshotConfiguration.Pose.innerLandscape, .tabletop, .outerPortrait] { - try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) + try await AdaptivePaneTestSupport.resize(controller, state: state, to: pose) #expect(await store.sampleReadCount == readsAfterYearChange) } await store.releaseFirstSamplesCall() diff --git a/Where/WhereUI/Tests/MainTabsTests.swift b/Where/WhereUI/Tests/MainTabsTests.swift index 48b154474..5acec8077 100644 --- a/Where/WhereUI/Tests/MainTabsTests.swift +++ b/Where/WhereUI/Tests/MainTabsTests.swift @@ -79,8 +79,8 @@ struct MainTabsTests { struct MainTabsTestsDuo { @Test func settingsSearchStaysVisibleAcrossFoldLayouts() async throws { let world = try await WhereFlyoverWorld.build() - let state = AdaptiveHistoryHostState() - let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + let state = AdaptivePaneHostState() + let host = UIHostingController(rootView: AdaptivePaneHostedSurface(state: state) { MainTabs(world: world, tab: .settings) .environment(world.model) .environment(world.session) @@ -96,10 +96,10 @@ struct MainTabsTestsDuo { .book, .outerPortrait, ] { - try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) + try await AdaptivePaneTestSupport.resize(controller, state: state, to: pose) let window = try #require(controller.view.window) try await waitForAsync { - AdaptiveHistoryTestSupport.descendants( + AdaptivePaneTestSupport.descendants( of: window, matching: UISearchTextField.self, ) @@ -114,7 +114,7 @@ struct MainTabsTestsDuo { .contains(frame) } } - let field = try #require(AdaptiveHistoryTestSupport.descendants( + let field = try #require(AdaptivePaneTestSupport.descendants( of: window, matching: UISearchTextField.self, ).first { !$0.isHidden && $0.window != nil }) @@ -131,8 +131,8 @@ struct MainTabsTestsDuo { @Test func foldingKeepsOnePresentedEditorAndItsUnsavedDate() async throws { let world = try await WhereFlyoverWorld.build() let navigation = LocationsView.Navigation() - let state = AdaptiveHistoryHostState() - let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + let state = AdaptivePaneHostState() + let host = UIHostingController(rootView: AdaptivePaneHostedSurface(state: state) { LocationsView(report: world.report, navigation: navigation) .bScrollRestorationScope() .environment(world.model) @@ -141,7 +141,7 @@ struct MainTabsTestsDuo { }) host.safeAreaRegions = [] try await show(host) { controller in - try await AdaptiveHistoryTestSupport.resize( + try await AdaptivePaneTestSupport.resize( controller, state: state, to: .outerPortrait, @@ -150,15 +150,15 @@ struct MainTabsTestsDuo { navigation.planningDestination = .new(.california) let root = try #require(controller.view.window?.rootViewController) try await waitForAsync { - AdaptiveHistoryTestSupport.presentedController(from: root) != nil + AdaptivePaneTestSupport.presentedController(from: root) != nil } - let editor = try #require(AdaptiveHistoryTestSupport.presentedController(from: root)) + let editor = try #require(AdaptivePaneTestSupport.presentedController(from: root)) defer { editor.dismiss(animated: false) } try await waitForAsync { - !AdaptiveHistoryTestSupport + !AdaptivePaneTestSupport .descendants(of: editor.view, matching: UIDatePicker.self).isEmpty } - let picker = try #require(AdaptiveHistoryTestSupport.descendants( + let picker = try #require(AdaptivePaneTestSupport.descendants( of: editor.view, matching: UIDatePicker.self, ).first) @@ -173,10 +173,10 @@ struct MainTabsTestsDuo { .innerLandscape, .outerPortrait, ] { - try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) - #expect(AdaptiveHistoryTestSupport.presentedController(from: root) === editor) + try await AdaptivePaneTestSupport.resize(controller, state: state, to: pose) + #expect(AdaptivePaneTestSupport.presentedController(from: root) === editor) #expect(navigation.selectedRegion == .california) - #expect(AdaptiveHistoryTestSupport.descendants( + #expect(AdaptivePaneTestSupport.descendants( of: editor.view, matching: UIDatePicker.self, ).contains { diff --git a/Where/WhereUI/Tests/PlannedStayEditorTests.swift b/Where/WhereUI/Tests/PlannedStayEditorTests.swift index f5f648512..cb62bc87c 100644 --- a/Where/WhereUI/Tests/PlannedStayEditorTests.swift +++ b/Where/WhereUI/Tests/PlannedStayEditorTests.swift @@ -13,8 +13,8 @@ struct PlannedStayEditorTests { let report = PlanningTestSupport.report(store: store) await report.refresh() let model = PlannedStayEditorModel(report: report, mode: .new(initialRegion: .california)) - let state = AdaptiveHistoryHostState() - let host = UIHostingController(rootView: AdaptiveHistoryHostedSurface(state: state) { + let state = AdaptivePaneHostState() + let host = UIHostingController(rootView: AdaptivePaneHostedSurface(state: state) { PlannedStayEditor(model: model) }) host.safeAreaRegions = [] @@ -31,7 +31,7 @@ struct PlannedStayEditorTests { .tabletop, .outerPortrait, ] { - try await AdaptiveHistoryTestSupport.resize(controller, state: state, to: pose) + try await AdaptivePaneTestSupport.resize(controller, state: state, to: pose) #expect(model.isSaving) #expect(model.region == .california) #expect(await store.plannedStayWriteCount == 1) diff --git a/Where/WhereUI/Tests/PresenceTimelineListTests.swift b/Where/WhereUI/Tests/PresenceTimelineListTests.swift index 4119f686f..75e2a8b66 100644 --- a/Where/WhereUI/Tests/PresenceTimelineListTests.swift +++ b/Where/WhereUI/Tests/PresenceTimelineListTests.swift @@ -25,7 +25,7 @@ struct PresenceTimelineListTests { calendar: report.calendar, ) let original = try #require(before.first) - let history = HistorySelectionModel() + let history = PresenceSelectionModel() history.select( month: CalendarDay(year: report.selectedYear, month: 2, day: 1), dateCalendar: report.calendar, @@ -59,7 +59,7 @@ struct PresenceTimelineListTests { @Test func selectingTheSameMonthAgainRevealsItAfterIndependentBrowsing() async throws { let world = try await WhereFlyoverWorld.build() let report = try #require(world.report.report) - let history = HistorySelectionModel() + let history = PresenceSelectionModel() let month = CalendarDay(year: report.year, month: 5, day: 1) let end = CalendarDay(year: report.year, month: 5, day: 31) let stints = PresenceTimeline.stints(from: report, calendar: world.report.calendar) @@ -109,7 +109,7 @@ struct PresenceTimelineListTests { PresenceTimelineList.self, role: PresenceTimelineList.ScrollRole.timeline, context: report.year, - idType: HistorySelectionModel.Item.self, + idType: PresenceSelectionModel.Item.self, capture: capture, ) }.whereBroadwayRoot().bScrollRestorationScope()) @@ -119,22 +119,22 @@ struct PresenceTimelineListTests { history.select(month: month, dateCalendar: world.report.calendar) try await waitForAsync { capture.binding?.wrappedValue - .viewID(type: HistorySelectionModel.Item.self) == .recorded(expected.id) + .viewID(type: PresenceSelectionModel.Item.self) == .recorded(expected.id) && HostedAccessibility.isVisible(expectedLabel, in: host.view) } #expect(history.pendingRequest(for: .timeline) == nil) let selectedInterval = history.selection capture.binding?.wrappedValue.scrollTo( - id: HistorySelectionModel.Item.recorded(browsed.id), + id: PresenceSelectionModel.Item.recorded(browsed.id), anchor: .top, ) try await waitForAsync { capture.binding?.wrappedValue - .viewID(type: HistorySelectionModel.Item.self) == .recorded(browsed.id) + .viewID(type: PresenceSelectionModel.Item.self) == .recorded(browsed.id) && HostedAccessibility.isVisible(browsedLabel, in: host.view) } #expect(capture.binding?.wrappedValue - .viewID(type: HistorySelectionModel.Item.self) == .recorded(browsed.id)) + .viewID(type: PresenceSelectionModel.Item.self) == .recorded(browsed.id)) #expect(history.selection == selectedInterval) } } diff --git a/Where/WhereUI/Tests/Support/AdaptiveHistoryLoadFixture.swift b/Where/WhereUI/Tests/Support/AdaptivePaneLoadFixture.swift similarity index 97% rename from Where/WhereUI/Tests/Support/AdaptiveHistoryLoadFixture.swift rename to Where/WhereUI/Tests/Support/AdaptivePaneLoadFixture.swift index e1d1caa92..458c06570 100644 --- a/Where/WhereUI/Tests/Support/AdaptiveHistoryLoadFixture.swift +++ b/Where/WhereUI/Tests/Support/AdaptivePaneLoadFixture.swift @@ -5,7 +5,7 @@ import SnapshotKit /// Drives the real report through controlled store reads. No presentation state is injected. @MainActor -final class AdaptiveHistoryLoadFixture { +final class AdaptivePaneLoadFixture { enum State: String, CaseIterable { case loading case empty diff --git a/Where/WhereUI/Tests/Support/CalendarPresentationFixture.swift b/Where/WhereUI/Tests/Support/CalendarPresentationFixture.swift index 94c92e4ae..84cb32e40 100644 --- a/Where/WhereUI/Tests/Support/CalendarPresentationFixture.swift +++ b/Where/WhereUI/Tests/Support/CalendarPresentationFixture.swift @@ -15,7 +15,7 @@ final class CalendarPresentationFixture { struct CalendarPresentationHarness: View { let fixture: CalendarPresentationFixture let report: YearReportModel - let history: HistorySelectionModel + let history: PresenceSelectionModel var body: some View { NavigationStack { From 99295e110badf225b5444693ec483bcac856c94c Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 12 Nov 2026 16:23:17 -0500 Subject: [PATCH 14/18] Limit region selection indicators to companion-pane layouts --- .../locations.ReturnedFromCalendar_iPhone.png | 3 ++ ...tions.ReturnedFromCalendar_iPhone_dark.png | 3 ++ .../Sources/Primary/LocationsCardsView.swift | 5 +- .../Sources/Primary/LocationsView.swift | 39 +++++++++++---- Where/WhereUI/Tests/LocationsViewTests.swift | 47 ++++++++++++++++++- 5 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ReturnedFromCalendar_iPhone.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ReturnedFromCalendar_iPhone_dark.png diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ReturnedFromCalendar_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ReturnedFromCalendar_iPhone.png new file mode 100644 index 000000000..7c3520a2f --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ReturnedFromCalendar_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2870e83f37c110da02b88f880b88af61f19d56f170c804fcf511391a7a6772c6 +size 2073994 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ReturnedFromCalendar_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ReturnedFromCalendar_iPhone_dark.png new file mode 100644 index 000000000..ed62276d3 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.ReturnedFromCalendar_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6fbe412066cf59a14b484e8102d240024d090161b01045ba6157ece9d8f333d +size 1994083 diff --git a/Where/WhereUI/Sources/Primary/LocationsCardsView.swift b/Where/WhereUI/Sources/Primary/LocationsCardsView.swift index a466e5e37..034dc365a 100644 --- a/Where/WhereUI/Sources/Primary/LocationsCardsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsCardsView.swift @@ -15,6 +15,7 @@ struct LocationsCardsView: View { let planningAction: () -> Void @Bindable var navigation: LocationsView.Navigation let cardVariant: WhereStylesheet.CardStyle.Variant + let showsSelection: Bool @BScrollPosition private var position: ScrollPosition enum ScrollRole: Hashable { case browse } @@ -29,6 +30,7 @@ struct LocationsCardsView: View { planningAction: @escaping () -> Void, navigation: LocationsView.Navigation, cardVariant: WhereStylesheet.CardStyle.Variant, + showsSelection: Bool, ) { self.report = report self.cardPresentation = cardPresentation @@ -39,6 +41,7 @@ struct LocationsCardsView: View { self.planningAction = planningAction self.navigation = navigation self.cardVariant = cardVariant + self.showsSelection = showsSelection _position = BScrollPosition( Self.self, role: ScrollRole.browse, @@ -124,7 +127,7 @@ struct LocationsCardsView: View { .locationCardRankingRegion(item.region) .id(item.region) .presenceSelection( - navigation.selectedRegion == item.region, + showsSelection && navigation.selectedRegion == item.region, cornerRadius: stylesheet.card[cardVariant].cornerRadius, ) } diff --git a/Where/WhereUI/Sources/Primary/LocationsView.swift b/Where/WhereUI/Sources/Primary/LocationsView.swift index faac00e27..2393e066b 100644 --- a/Where/WhereUI/Sources/Primary/LocationsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsView.swift @@ -98,6 +98,7 @@ struct LocationsView: View { tilt: tilt, calendarTransition: calendarTransition, cardVariant: arrangement == .compact ? .regular : .column, + showsSelection: arrangement != .compact, openElsewhere: openElsewhere, ) .navigationSplitViewColumnWidth( @@ -143,6 +144,7 @@ struct LocationsView: View { cardPresentation: cardPresentation, tilt: tilt, calendarTransition: calendarTransition, + showsSelection: true, openElsewhere: openElsewhere, ) } @@ -210,6 +212,7 @@ struct LocationsBrowsePane: View { @Namespace private var localTransition private let transition: Namespace.ID? let cardVariant: WhereStylesheet.CardStyle.Variant + let showsSelection: Bool init( report: YearReportModel, @@ -218,6 +221,7 @@ struct LocationsBrowsePane: View { tilt: TiltProvider? = nil, calendarTransition: Namespace.ID? = nil, cardVariant: WhereStylesheet.CardStyle.Variant = .regular, + showsSelection: Bool = false, openElsewhere: (() -> Void)? = nil, ) { self.report = report @@ -230,6 +234,7 @@ struct LocationsBrowsePane: View { _tilt = State(initialValue: tilt ?? TiltProvider()) transition = calendarTransition self.cardVariant = cardVariant + self.showsSelection = showsSelection } var body: some View { @@ -242,6 +247,7 @@ struct LocationsBrowsePane: View { tilt: tilt, calendarTransition: transition ?? localTransition, cardVariant: cardVariant, + showsSelection: showsSelection, openElsewhere: openElsewhere, ) .navigationDestination(for: LocationsView.Navigation.Destination @@ -266,6 +272,7 @@ struct LocationsContent: View { let report: YearReportModel var openElsewhere: (() -> Void)? let cardVariant: WhereStylesheet.CardStyle.Variant + let showsSelection: Bool @State private var navigation: LocationsView.Navigation @State private var isCardSurfaceVisible = false @@ -299,12 +306,14 @@ struct LocationsContent: View { tilt: TiltProvider? = nil, calendarTransition: Namespace.ID? = nil, cardVariant: WhereStylesheet.CardStyle.Variant = .regular, + showsSelection: Bool = false, openElsewhere: (() -> Void)? = nil, ) { _navigation = State(initialValue: navigation ?? LocationsView.Navigation()) self.report = report self.openElsewhere = openElsewhere self.cardVariant = cardVariant + self.showsSelection = showsSelection transition = calendarTransition _tilt = State(initialValue: tilt ?? TiltProvider()) _cardPresentation = State(initialValue: cardPresentation ?? LocationCardsPresentationModel( @@ -379,6 +388,7 @@ struct LocationsContent: View { planningAction: openPlanning, navigation: navigation, cardVariant: cardVariant, + showsSelection: showsSelection, ) } } @@ -454,6 +464,12 @@ private struct ResolveToolbarLabel: View { .browseColumn)], cardVariant: .column, ) + artworkSnapshot( + name: "ReturnedFromCalendar", + report: PreviewSupport.loadedYearReportModel(), + configurations: .fullContentPhoneLightDark, + selectedRegion: .california, + ) artworkSnapshot( name: "Itinerary", report: PreviewSupport.itineraryYearReportModel(), @@ -551,6 +567,7 @@ private struct ResolveToolbarLabel: View { theme: WhereTheme = .standard, reduceTransparency: Bool = false, cardVariant: WhereStylesheet.CardStyle.Variant = .regular, + selectedRegion: Region? = nil, openElsewhere _: (() -> Void)? = nil, ) -> SnapshotCase { let cache = RegionOutlinePathCache() @@ -571,15 +588,21 @@ private struct ResolveToolbarLabel: View { } }, ) { - LocationsBrowsePane(report: report, cardVariant: cardVariant) - .environment(\.regionOutlinePathCache, cache) - .bTraitOverrides { traits, overrides in - if reduceTransparency { - var accessibility = traits.accessibility - accessibility.isReduceTransparencyEnabled = true - overrides.accessibility = accessibility - } + let navigation = LocationsView.Navigation() + let _ = navigation.selectedRegion = selectedRegion + LocationsBrowsePane( + report: report, + navigation: navigation, + cardVariant: cardVariant, + ) + .environment(\.regionOutlinePathCache, cache) + .bTraitOverrides { traits, overrides in + if reduceTransparency { + var accessibility = traits.accessibility + accessibility.isReduceTransparencyEnabled = true + overrides.accessibility = accessibility } + } } } diff --git a/Where/WhereUI/Tests/LocationsViewTests.swift b/Where/WhereUI/Tests/LocationsViewTests.swift index 97f381b20..2806231b4 100644 --- a/Where/WhereUI/Tests/LocationsViewTests.swift +++ b/Where/WhereUI/Tests/LocationsViewTests.swift @@ -1,3 +1,4 @@ +import AccessibilitySnapshotParser import BroadwayUI import RegionKit import SnapshotKit @@ -17,6 +18,43 @@ struct LocationsViewTests { @MainActor @Suite(.serialized) struct LocationsViewTestsDuo { + @Test func selectedRegionIsIndicatedOnlyBesideItsCalendar() async throws { + let world = try await WhereFlyoverWorld.build() + let navigation = LocationsView.Navigation() + let state = AdaptivePaneHostState() + let host = UIHostingController(rootView: AdaptivePaneHostedSurface(state: state) { + LocationsView(report: world.report, navigation: navigation) + }.bScrollRestorationScope()) + host.safeAreaRegions = [] + try await show(host) { controller in + controller.view.window?.layer.speed = 1 + navigation.select(region: .california) + // Returning to browsing retains the selected region for the next unfold. + navigation.compactColumn = .sidebar + for pose in [ + SnapshotConfiguration.Pose.outerPortrait, + .innerLandscape, + .tabletop, + .outerPortrait, + ] { + try await AdaptivePaneTestSupport.resize(controller, state: state, to: pose) + try await waitForStage("California card after \(pose)") { + HostedAccessibility.objects(in: controller.view).contains { + $0.accessibilityLabel?.hasPrefix("California:") == true + && $0.accessibilityTraits.contains(.button) + && $0.accessibilityFrame.intersects(controller.view.convert( + controller.view.bounds, + to: nil, + )) + && $0.accessibilityTraits + .contains(.selected) == (pose != .outerPortrait) + } + } + #expect(navigation.selectedRegion == .california) + } + } + } + @Test func initialBrowsePositionFollowsInsetsUntilARegionIsRestored() async throws { let world = try await WhereFlyoverWorld.build() let navigation = LocationsView.Navigation() @@ -60,10 +98,15 @@ struct LocationsViewTestsDuo { } for topInset in [CGFloat(82), 0, 56, 0] { let previousInset = scroll.adjustedContentInset.top + let previousHostInset = controller.view.safeAreaInsets.top controller.additionalSafeAreaInsets.top = topInset controller.view.setNeedsLayout() - controller.view.window?.layoutIfNeeded() + // Lay out the host whose safe area changed, not only its window. + controller.view.layoutIfNeeded() do { + try await waitForStage("host safe area after requested inset \(topInset)") { + abs(controller.view.safeAreaInsets.top - previousHostInset) > 1 + } try await waitForStage( "top anchoring after inset \(previousInset) -> requested \(topInset)", ) { @@ -73,7 +116,7 @@ struct LocationsViewTestsDuo { } } catch { throw TestHostError( - "\(error); adjusted=\(scroll.adjustedContentInset), offset=\(scroll.contentOffset), bounds=\(scroll.bounds), content=\(scroll.contentSize), attached=\(scroll.window != nil), current=\(String(describing: currentScroll()))", + "\(error); requested=\(controller.additionalSafeAreaInsets), hostSafeArea=\(controller.view.safeAreaInsets), scrollSafeArea=\(scroll.safeAreaInsets), adjusted=\(scroll.adjustedContentInset), offset=\(scroll.contentOffset), bounds=\(scroll.bounds), content=\(scroll.contentSize), attached=\(scroll.window != nil), current=\(String(describing: currentScroll()))", ) } let current = try #require(currentScroll()) From 5412e318c8fca709cdd74281e61a7681e95dc8fd Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 12 Nov 2026 16:27:49 -0500 Subject: [PATCH 15/18] Restore the standard Your Year selector at every size --- Where/WhereUI/AGENTS.md | 2 + Where/WhereUI/README.md | 11 +- ...e-Year-book-Selected_duoInnerLandscape.png | 3 - ...e-Year-book-Timeline_duoInnerLandscape.png | 3 + ...s.Adaptive-Year-book_duoInnerLandscape.png | 4 +- ...-Year-innerLandscape_duoInnerLandscape.png | 4 +- ...nnerPortrait-Selected_duoInnerPortrait.png | 3 - ...nnerPortrait-Timeline_duoInnerPortrait.png | 3 + ...ve-Year-innerPortrait_duoInnerPortrait.png | 4 +- ...ortrait_duoInnerPortrait_accessibility.png | 4 +- ...ar-innerPortrait_duoInnerPortrait_dark.png | 4 +- ...e-Year-tabletop-Month_duoInnerPortrait.png | 3 - ...ear-tabletop-Selected_duoInnerPortrait.png | 3 - ...ear-tabletop-Timeline_duoInnerPortrait.png | 3 + ...daptive-Year-tabletop_duoInnerPortrait.png | 4 +- ...s.UpdateAvailable_iPhone_accessibility.png | 4 +- ...omeActionRequired_iPhone_accessibility.png | 4 +- ...s.WelcomeLocating_iPhone_accessibility.png | 4 +- ...nnerPortrait_duoInnerPortrait_contrast.png | 4 +- ...ear-innerPortrait_duoInnerPortrait_rtl.png | 4 +- ...e-empty-innerPortrait_duoInnerPortrait.png | 4 +- ...aptive-empty-tabletop_duoInnerPortrait.png | 4 +- ...-failed-innerPortrait_duoInnerPortrait.png | 4 +- ...ptive-failed-tabletop_duoInnerPortrait.png | 4 +- ...loading-innerPortrait_duoInnerPortrait.png | 4 +- ...tive-loading-tabletop_duoInnerPortrait.png | 4 +- .../year.Loaded_iPad_accessibility.png | 4 +- .../year.Loaded_iPhone_accessibility.png | 4 +- .../DuoLayout/DuoLayoutSnapshotFixture.swift | 34 +---- Where/WhereUI/Sources/MainTabs.swift | 21 ++-- Where/WhereUI/Sources/Year/YearView.swift | 119 +----------------- Where/WhereUI/Tests/YearViewTests.swift | 64 ++++++++++ 32 files changed, 131 insertions(+), 217 deletions(-) delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Timeline_duoInnerLandscape.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Timeline_duoInnerPortrait.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png create mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Timeline_duoInnerPortrait.png create mode 100644 Where/WhereUI/Tests/YearViewTests.swift diff --git a/Where/WhereUI/AGENTS.md b/Where/WhereUI/AGENTS.md index 2f5184422..b0083a79e 100644 --- a/Where/WhereUI/AGENTS.md +++ b/Where/WhereUI/AGENTS.md @@ -160,6 +160,8 @@ Layering, localization, preview, and testing conventions live in the feature reveal stays covered when headless promotion coalesces or the first hold is interrupted, while warm resumes never replay it. - Measure calendar month sections, day rows, and Timeline rows eagerly so full-content measurement and scroll restoration use their actual heights. Guards: `CalendarContentViewSnapshotTests`, `PresenceTimelineListSnapshotTests`. +- Keep Your Year in one navigation stack with its bottom calendar/timeline selector at every size. Guard: `YearViewTests`. +- Show Locations selection indicators only when browsing and calendar panes are displayed together. Guard: `LocationsViewTestsDuo.selectedRegionIsIndicatedOnlyBesideItsCalendar`. - Keep navigation paths, search, sheets, and drafts in stable owning views above adaptive branches. Keep active drill-in stacks mounted while folding; defer tabletop composition until returning. - Read system reserved regions only in `AdaptivePaneGeometry`. Keep authored pane limits in the stylesheet and measured geometry in the adapter. Guard: `where.reserved_region_layout_ownership`. - Keep explicit reveal requests in the owning view, with typed request identities distinct from content identities. Use BroadwayUI scroll restoration for current-context anchors; scrolling must not select or move another pane. diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index a79656601..8ad267c7d 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -86,19 +86,20 @@ A report-year change waits for matching data before positioning. Stale visibilit Calendar sections and day rows measure eagerly so offscreen estimates cannot move a restored month. The timeline keeps its pinned overview outside the scrolling rows; large text places it in the content flow. Timeline stays also measure eagerly so resizing cannot shift the content origin. -Timeline anchors refer to displayed stay segments or flight identities. Month -selection reveals the first overlapping displayed entry. +Timeline anchors refer to displayed stay segments or flight identities. +Switching between the calendar and timeline restores each view independently. On a wide window, Locations shows cards beside the selected region's calendar. Without a region selection, the detail pane shows the year calendar. -Your Year shows the calendar and timeline together. Settings uses native list and detail navigation. +Your Year uses one navigation stack and the bottom calendar/timeline selector at every window size. +Settings uses native list and detail navigation. Settings keeps its native search field visible in the navigation drawer across compact and split layouts. -Narrow windows and accessibility text sizes use one pane. The compact calendar/timeline choice remains stored while both panes are visible. +Narrow windows and accessibility text sizes use one pane. Locations marks the selected region only while its browsing and calendar panes are shown together. Theme choices stack when the available pane cannot fit two readable card specimens. `AdaptivePaneGeometry` reads independent edge insets and the system's reserved regions. `AdaptivePaneArrangement` combines that geometry with the stylesheet's pane limits. -An active horizontal division places the selected month above the hinge and browsing controls below it. +In Locations, an active horizontal division places the selected month above the hinge and browsing controls below it. A vertical division retains native browse/detail navigation. Native containers own system-bar placement. The adapter uses container geometry, without device-name or hinge-angle checks. diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png deleted file mode 100644 index f0b1647e0..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Selected_duoInnerLandscape.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c5254057dd249e4534b9f98c906797348a7513dc2478200ccf3d4e5a049c58cb -size 723817 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Timeline_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Timeline_duoInnerLandscape.png new file mode 100644 index 000000000..c2598e784 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book-Timeline_duoInnerLandscape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71d3d229119f5c58327d5f5ee3d7e53e793ee5935ed9d5c9a4cc8d4fb5785258 +size 2646461 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png index ea06aad42..9627e8e3e 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-book_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e18a6ea5227bd10c01e847f678c154bb0f92227a516017fa50fd6d68e11ca494 -size 2654271 +oid sha256:23b616d5d790add70a8f384b3c48fc99245de2ceb2a4d2cefe81af1fd45e7218 +size 2968589 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png index ea06aad42..28cb94c95 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerLandscape_duoInnerLandscape.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e18a6ea5227bd10c01e847f678c154bb0f92227a516017fa50fd6d68e11ca494 -size 2654271 +oid sha256:b556cfbe7c7c30fbc301a235df0e1f413bd2a3f61d4824348e36389c286fa16d +size 2968594 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png deleted file mode 100644 index 860b18a30..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Selected_duoInnerPortrait.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:34620447e74c7d45651bcbfd312e504d1bf8ebe354fe5d5ec805982cda44295e -size 817661 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Timeline_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Timeline_duoInnerPortrait.png new file mode 100644 index 000000000..d619d3939 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait-Timeline_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3e2c11c820c06f1c2b7d2ac658226b4bbcbf5e205e725892728494ee11388d7 +size 2538795 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png index da220b290..4f983f48f 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c55729b7b5d0988e62c546cc5375d0961b54000e315df672a26e08eb18db8ea5 -size 2305985 +oid sha256:9414de886c4b6593194a071ea8533a0cbee43843a6626c8803d7f1418b3095ba +size 2616476 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png index d4a8c7163..eedf55ec3 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5867f52f117f2bab39877e0e59decda7315de552386bb8fbc70a2a04dde51f1 -size 2986707 +oid sha256:a2221141eeafbd5eb025788777fe207df5782dc203ecdfdcde3a08039ce265ed +size 3170831 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png index e2e622310..611dc0e5d 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:320ef528f073749ecbf29ad093aad024f7557f3d523c77758246808b15fd3220 -size 2685842 +oid sha256:fc59076e69e6e546b2cf2d6e4295c74c1eb53ed713067a24a070ff139aeed0ee +size 1858680 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png deleted file mode 100644 index dc8f66a13..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Month_duoInnerPortrait.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:16c8779617a288a6fd4ce47119438597d3ee0f4dce12435e42e09a2b59b51f4d -size 697521 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png deleted file mode 100644 index be2e4a9f7..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Selected_duoInnerPortrait.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0dc2463d9a4d50577abf4817348c175846923b9bc69185e2e016bc7b1680c2e1 -size 479665 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Timeline_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Timeline_duoInnerPortrait.png new file mode 100644 index 000000000..d619d3939 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop-Timeline_duoInnerPortrait.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3e2c11c820c06f1c2b7d2ac658226b4bbcbf5e205e725892728494ee11388d7 +size 2538795 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png index 8373c7495..4f983f48f 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.Adaptive-Year-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0139e1064caefe1ea706584b039d293fae9c002590dbeb7b5c0c82ee65de3911 -size 605529 +oid sha256:9414de886c4b6593194a071ea8533a0cbee43843a6626c8803d7f1418b3095ba +size 2616476 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.UpdateAvailable_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.UpdateAvailable_iPhone_accessibility.png index 412ef0a15..35bb942c4 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.UpdateAvailable_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.UpdateAvailable_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:985d486479cb507b259df867d939d07667604213c34b04935a157671b6a02cb8 -size 375333 +oid sha256:bcdf6b1b52c22f06ae1272ac3e84e94aa73c7ba5678bddc6bdaff88394281a40 +size 373080 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeActionRequired_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeActionRequired_iPhone_accessibility.png index 51194a94d..8d8c77676 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeActionRequired_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeActionRequired_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc802ca508bff4f86ec34850be990244cc006d2eca46a69f5cdf1613c2142b04 -size 398366 +oid sha256:42debe38ee3ef2129735deade42c660ce22b3268e9e0c59cfe75b9991c334ee2 +size 395931 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocating_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocating_iPhone_accessibility.png index 676eaaf67..a52b7f152 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocating_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/mainTabs.WelcomeLocating_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdd59124389083711b4c32b4055fa544407cc7edc06a02a3780fc5a0aefbc532 -size 393480 +oid sha256:e7b4649c306908618b4dc9755397214d12ceb5375625cc08b03775d565646516 +size 390782 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png index 01154b891..ffe627554 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbf5b037edfb4bcc7ead999cacd50661f78f442d475345d8af043db0ed5c35b1 -size 2341397 +oid sha256:67f348f5500ee298dd479ebc09753813d7356cae5f8c60b5115d43e133b4e1ef +size 2596126 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png index 6d7a6bb45..1b5b328f5 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Adaptive-Year-innerPortrait_duoInnerPortrait_rtl.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0de9ca45024be2aeea6b351dc0b9fb2e55f2ceee7acb6305dad1779a0cb1e345 -size 2318890 +oid sha256:fa3a0f86c167d2f4ae72eb11bfa7b510cc945ea730219a1700fbd909ee54ce93 +size 2614252 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png index 71013be54..b34d3ba00 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e413d2f09dd061a638c32c128b6e6d968c76cc61d5afb04ec294afb6c0fb925a -size 539879 +oid sha256:eb69591909f3d791c2a346a5b3c98dceee0c95a5b03067be05ddaa7296a71a21 +size 497750 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png index a092e86e0..b34d3ba00 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-empty-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0583df4a7fc8402412a9a1fce12457aedac26145e13800bc22ff64292240b78 -size 334639 +oid sha256:eb69591909f3d791c2a346a5b3c98dceee0c95a5b03067be05ddaa7296a71a21 +size 497750 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png index 2fb83cd04..dc96ca488 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea003378d649d766c20ce38cd62ed89e8c136e10596835f67b22966ca31805b4 -size 195988 +oid sha256:db53cf16f21409a327164922bf8d67a6db4cc2f8dcb61369a7aeb071ad13aa98 +size 182481 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png index d3afd4c89..dc96ca488 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-failed-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9901b410c688acab07e9c37ccfbb29faeced49468ff03d7751c89389e272975 -size 290721 +oid sha256:db53cf16f21409a327164922bf8d67a6db4cc2f8dcb61369a7aeb071ad13aa98 +size 182481 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png index 952a8561d..11b3d95ef 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-innerPortrait_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6f6109cc2dd79a2707e8b51fea06d1532971f09bf4cfb7228bfe04b28bbde43 -size 369791 +oid sha256:399d9e353f3b4ea65bf488e4b869518c2d92090f636ce259ad4455f714b8a2cc +size 323895 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png index 3b44c3a66..11b3d95ef 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/adaptiveLoadStates.Adaptive-loading-tabletop_duoInnerPortrait.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c23c394dadf9a859b8fdd45aff32cb08778e7f5436d8b579c8874400b55af2d -size 569290 +oid sha256:399d9e353f3b4ea65bf488e4b869518c2d92090f636ce259ad4455f714b8a2cc +size 323895 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_accessibility.png index cbf850e59..13d475bba 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95fc4519d24263a9a43ef1ec4d748e95734638d20ed0a8a458c9451e29fc834f -size 4180789 +oid sha256:175851eff1c337217cb336ea5c8e264bcc0720cf1143f275dc80d6f14bcaa6d8 +size 4171506 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_accessibility.png index 4ccfa2da9..55d0e6351 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd97d9edca4821104e158a96232a9c0efcc9b001ed87b814efb3850cf32782d4 -size 2723534 +oid sha256:16e6782477bdf112e6f4a0af8fe02a519f55335f3c1af3edc5349826218153c1 +size 2714048 diff --git a/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutSnapshotFixture.swift b/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutSnapshotFixture.swift index c68561f36..14823ffca 100644 --- a/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutSnapshotFixture.swift +++ b/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutSnapshotFixture.swift @@ -30,17 +30,14 @@ enum Selection: Equatable { case none case region - case stay - case month(CalendarDay) + case timeline case settings } let model = DuoLayoutLabModel() let artwork = RegionOutlinePathCache() let tab: MainTabSelection.TabID - let history = PresenceSelectionModel() let selection: Selection - private var didSelect = false init(tab: MainTabSelection.TabID, selection: Selection) { self.tab = tab @@ -54,33 +51,6 @@ await Task.yield() } guard case let .loaded(world) = model.state else { return } - if !didSelect { - didSelect = true - switch selection { - case .none: break - case .region: - break - case .stay: - if let report = world.report.report, - let stint = PresenceTimeline.stints( - from: report, - calendar: world.report.calendar, - ).first - { - history.select( - stint: stint, - dateCalendar: world.report.calendar, - ) - } - case let .month(month): - history.select( - month: month, - dateCalendar: world.report.calendar, - ) - case .settings: - break - } - } for region in LocationsBackgroundArtwork.regions(in: world.report.ranking) { _ = await artwork.path(for: region, resolution: .small) _ = await artwork.path(for: region, resolution: .medium) @@ -114,7 +84,7 @@ region: fixture.selection == .region ? .california : nil, settingsRoute: fixture .selection == .settings ? SettingsRoute(.appearance) : nil, - history: fixture.history, + yearMode: fixture.selection == .timeline ? .timeline : .calendar, ) .environment(world.model) .environment(world.session) diff --git a/Where/WhereUI/Sources/MainTabs.swift b/Where/WhereUI/Sources/MainTabs.swift index e3a1cf378..2e6fc4269 100644 --- a/Where/WhereUI/Sources/MainTabs.swift +++ b/Where/WhereUI/Sources/MainTabs.swift @@ -36,7 +36,7 @@ struct MainTabs: View { @State private var selection = MainTabSelection() private var initialRegion: Region? private var initialSettingsRoute: SettingsRoute? - private var initialHistory: PresenceSelectionModel? + private var initialYearMode: YearMode = .calendar @Environment(UpdateModel.self) private var updates: UpdateModel? @Environment(\.isInDemoMode) private var isInDemoMode @Environment(\.scenePhase) private var scenePhase @@ -95,7 +95,7 @@ struct MainTabs: View { } Tab(String(localized: .tabYear), systemSymbol: .calendar, value: TabID.year) { - YearView(report: report, history: initialHistory) + YearView(report: report, initialMode: initialYearMode) .reportingDeveloperTabBarInset() } @@ -227,7 +227,7 @@ struct MainTabs: View { tab: TabID = .locations, region: Region? = nil, settingsRoute: SettingsRoute? = nil, - history: PresenceSelectionModel? = nil, + yearMode: YearMode = .calendar, ) { self.init( session: world.session, @@ -241,7 +241,7 @@ struct MainTabs: View { ) initialRegion = region initialSettingsRoute = settingsRoute - initialHistory = history + initialYearMode = yearMode } #endif } @@ -389,7 +389,7 @@ struct MainTabs: View { tab: .year, pose: pose, configuration: SnapshotConfiguration(device: pose.frame), - selection: .stay, + selection: .timeline, )) } cases.append(adaptiveSnapshot( @@ -398,13 +398,6 @@ struct MainTabs: View { configuration: SnapshotConfiguration(device: .duoInnerPortrait), selection: .settings, )) - // May 2026 occupies six rows in the demo's Sunday-first calendar. - cases.append(adaptiveSnapshot( - tab: .year, - pose: .tabletop, - configuration: SnapshotConfiguration(device: .duoInnerPortrait), - selection: .month(CalendarDay(year: PreviewSupport.year, month: 5, day: 1)), - )) return cases } @@ -423,8 +416,8 @@ struct MainTabs: View { let fixture = DuoLayoutSnapshotFixture(tab: tab, selection: selection) let selectionName = switch selection { case .none: "" - case .region, .stay, .settings: "-Selected" - case .month: "-Month" + case .region, .settings: "-Selected" + case .timeline: "-Timeline" } return whereSnapshot( name: "Adaptive-" + tabName + "-" + pose.rawValue + selectionName, diff --git a/Where/WhereUI/Sources/Year/YearView.swift b/Where/WhereUI/Sources/Year/YearView.swift index 14e39a8ec..2e488c27c 100644 --- a/Where/WhereUI/Sources/Year/YearView.swift +++ b/Where/WhereUI/Sources/Year/YearView.swift @@ -11,7 +11,6 @@ struct YearView: View { let report: YearReportModel @State private var mode: YearMode - @State private var history: PresenceSelectionModel @State private var planningDestination: PlannedStaysDestination? @State private var calendarPresentation: CalendarPresentationState @@ -20,126 +19,15 @@ struct YearView: View { init( report: YearReportModel, initialMode: YearMode = .calendar, - history: PresenceSelectionModel? = nil, ) { self.report = report _mode = State(initialValue: initialMode) - _history = State(initialValue: history ?? PresenceSelectionModel()) _calendarPresentation = State(initialValue: CalendarPresentationState(mode: CalendarModel( report: report, persistenceKey: .year, ).isOverview(fallback: false) ? .overview : .detail)) } - var body: some View { - AdaptivePaneContainer { arrangement in - switch arrangement { - case .compact: - YearCompactContent( - report: report, - mode: $mode, - history: history, - calendarPresentation: calendarPresentation, - planningDestination: $planningDestination, - ) - case .columns: - NavigationSplitView(columnVisibility: .constant(.all)) { - timeline(presentation: .linked) - .navigationSplitViewColumnWidth( - min: stylesheet.adaptivePane.minimumControlsWidth, - ideal: stylesheet.adaptivePane.minimumControlsWidth, - ) - } detail: { - calendar(presentation: .year) - .navigationSplitViewColumnWidth( - min: stylesheet.adaptivePane.minimumDetailWidth, - ideal: stylesheet.adaptivePane.minimumDetailWidth, - ) - } - .navigationSplitViewStyle(.balanced) - case let .tabletop(panes): - TabletopPaneLayout(panes: panes) { - NavigationStack { calendar(presentation: .month) } - } controls: { - NavigationStack { - timeline(presentation: .linked) - .safeAreaInset(edge: .top, spacing: 0) { - MonthNavigationControls( - report: report, - history: history, - ) - } - } - } - } - } - .modifier(PlanningPresentation(report: report, destination: $planningDestination)) - .onChange(of: report.selectedYear) { history.reset() } - } - - private func calendar(presentation: CalendarContentView.Presentation) -> some View { - CalendarContentView( - report: report, - history: history, - presentationState: calendarPresentation, - presentation: presentation, - persistenceKey: .year, - scrollRole: .year, - planningDestination: $planningDestination, - ) - .navigationTitle(String(localized: .primaryCalendar)) - .navigationBarTitleDisplayMode(.inline) - } - - private func timeline(presentation: PresenceTimelineList.Presentation) -> some View { - PresenceTimelineList( - report: report, - presentation: presentation, - history: history, - planningDestination: $planningDestination, - ) - .navigationTitle(String(localized: .primaryTimeline)) - .navigationBarTitleDisplayMode(.inline) - } -} - -/// The single-pane production presentation, also captured in full for content completeness. -struct YearCompactView: View { - let report: YearReportModel - @State private var mode = YearMode.calendar - @State private var history = PresenceSelectionModel() - @State private var planningDestination: PlannedStaysDestination? - @State private var calendarPresentation: CalendarPresentationState - - init(report: YearReportModel) { - self.report = report - _calendarPresentation = State(initialValue: CalendarPresentationState(mode: CalendarModel( - report: report, - persistenceKey: .year, - ).isOverview(fallback: false) ? .overview : .detail)) - } - - var body: some View { - YearCompactContent( - report: report, - mode: $mode, - history: history, - calendarPresentation: calendarPresentation, - planningDestination: $planningDestination, - ) - .bScrollRestorationScope() - .modifier(PlanningPresentation(report: report, destination: $planningDestination)) - } -} - -private struct YearCompactContent: View { - let report: YearReportModel - @Binding var mode: YearMode - let history: PresenceSelectionModel - let calendarPresentation: CalendarPresentationState - @Binding var planningDestination: PlannedStaysDestination? - @Environment(\.stylesheet) private var stylesheet - var body: some View { NavigationStack { Group { @@ -147,7 +35,6 @@ private struct YearCompactContent: View { case .calendar: CalendarContentView( report: report, - history: history, presentationState: calendarPresentation, persistenceKey: .year, scrollRole: .year, @@ -156,7 +43,6 @@ private struct YearCompactContent: View { case .timeline: PresenceTimelineList( report: report, - history: history, planningDestination: $planningDestination, ) } @@ -175,6 +61,7 @@ private struct YearCompactContent: View { .padding(.bottom, stylesheet.spacing.xLarge) } } + .modifier(PlanningPresentation(report: report, destination: $planningDestination)) } } @@ -268,14 +155,14 @@ private struct YearModePicker: View { measurementReadiness: .immediate, settle: .settledAtLeast(minDuration: 1.0), ) { - YearCompactView(report: PreviewSupport.loadedYearReportModel()) + YearView(report: PreviewSupport.loadedYearReportModel()) } whereSnapshot( name: "Empty", configurations: .fullContentPhoneLightDark, measurementReadiness: .immediate, ) { - YearCompactView(report: PreviewSupport.emptyYearReportModel()) + YearView(report: PreviewSupport.emptyYearReportModel()) } whereSnapshot( name: "RestoredOverview", diff --git a/Where/WhereUI/Tests/YearViewTests.swift b/Where/WhereUI/Tests/YearViewTests.swift new file mode 100644 index 000000000..640c6ec9b --- /dev/null +++ b/Where/WhereUI/Tests/YearViewTests.swift @@ -0,0 +1,64 @@ +import AccessibilitySnapshotParser +import BroadwayUI +import SnapshotKit +import SwiftUI +import TestHostSupport +import Testing +@testable import WhereUI + +@MainActor +struct YearViewTests { + @Test func bottomSelectorAndSelectedModeSurviveEveryFoldLayout() async throws { + let world = try await WhereFlyoverWorld.build() + let state = AdaptivePaneHostState() + let host = UIHostingController(rootView: AdaptivePaneHostedSurface(state: state) { + YearView(report: world.report) + }.bScrollRestorationScope()) + host.safeAreaRegions = [] + try await show(host) { controller in + controller.view.window?.layer.speed = 1 + var retainedMode = YearMode.calendar + for pose in SnapshotConfiguration.Pose.allCases + [.outerPortrait] { + try await AdaptivePaneTestSupport.resize(controller, state: state, to: pose) + try await waitForAsync { + button(retainedMode, in: controller.view)?.accessibilityTraits + .contains(.selected) == true + } + for mode in [YearMode.calendar, .timeline] { + try await waitForAsync { self.button(mode, in: controller.view) != nil } + let button = try #require(self.button(mode, in: controller.view)) + if let control = button as? UIControl { + control.sendActions(for: .touchUpInside) + } else { + try #require(button.accessibilityActivate()) + } + try await waitForAsync { + self.button(mode, in: controller.view)?.accessibilityTraits + .contains(.selected) == true + && navigationControllers(in: controller) + .contains { $0.navigationBar.topItem?.title == mode.title } + } + #expect(navigationControllers(in: controller).count == 1) + } + retainedMode = .timeline + // Timeline is selected before the next resize; the same view owns it. + #expect(button(.timeline, in: controller.view)?.accessibilityTraits + .contains(.selected) == true) + } + } + } + + private func button(_ mode: YearMode, in view: UIView) -> NSObject? { + HostedAccessibility.objects(in: view).first { + $0.accessibilityLabel == mode.title && $0.accessibilityTraits.contains(.button) + && $0.accessibilityFrame.intersects(view.convert(view.bounds, to: nil)) + } + } + + private func navigationControllers(in controller: UIViewController) + -> [UINavigationController] + { + (controller as? UINavigationController).map { [$0] } ?? controller.children + .flatMap { navigationControllers(in: $0) } + } +} From 4c0cd0a1231c9f8b26368b72fbf0314247237db3 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 12 Nov 2026 16:47:32 -0500 Subject: [PATCH 16/18] Keep month reveals in Locations and timeline identity in its view --- Where/WhereUI/README.md | 23 +- .../LocationsViewSnapshotTests.swift | 2 +- .../SnapshotTests/YearViewSnapshotTests.swift | 2 +- ...enceTimeline.BrowseColumn_browseColumn.png | 3 - .../DuoLayout/DuoLayoutLabView.swift | 8 +- .../DuoLayout/DuoLayoutSnapshotFixture.swift | 2 +- Where/WhereUI/Sources/MainTabs.swift | 8 +- .../Sources/Primary/CalendarContentView.swift | 39 ++-- .../Sources/Primary/CalendarMonthReveal.swift | 15 ++ .../Sources/Primary/LocationsCardsView.swift | 2 +- .../Sources/Primary/LocationsView.swift | 10 +- .../Primary/MonthNavigationControls.swift | 20 +- .../Primary/PresenceTimelineList.swift | 110 +++------- .../Tests/CalendarContentViewTests.swift | 96 +++++++-- .../Tests/CalendarRevealTestSupport.swift | 18 ++ Where/WhereUI/Tests/LocationsViewTests.swift | 12 +- Where/WhereUI/Tests/MainTabsTests.swift | 4 +- .../Tests/PlannedStayEditorTests.swift | 2 +- .../Tests/PresenceTimelineListTests.swift | 196 ++++++++---------- .../Tests/ScrollPositionTestSupport.swift | 6 +- .../Support/CalendarPresentationFixture.swift | 6 +- Where/WhereUI/Tests/YearViewTests.swift | 2 +- 22 files changed, 303 insertions(+), 283 deletions(-) delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.BrowseColumn_browseColumn.png create mode 100644 Where/WhereUI/Sources/Primary/CalendarMonthReveal.swift create mode 100644 Where/WhereUI/Tests/CalendarRevealTestSupport.swift diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index 8ad267c7d..bd544a22e 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -103,15 +103,20 @@ In Locations, an active horizontal division places the selected month above the A vertical division retains native browse/detail navigation. Native containers own system-bar placement. The adapter uses container geometry, without device-name or hinge-angle checks. -`PresenceSelectionModel` links explicit month and stay selections through typed day intervals. -It highlights the interval and reveals it in the companion pane. Scroll observations only retain local anchors. -Each month reveal has a typed request identity. Consuming it preserves that identity, so repeated selections work without an observer feedback loop. -Planning sheets sit above the changing containers, with one shared presentation owner per destination. -The developer overlay and welcome overlay use the same geometry adapter to find clear space. - -The DEBUG **Duo Layout Lab** retains one production demo world while its pose control changes bounds, traits, and synthetic regions. -Embedded demo tools omit the session's Exit Demo action. The containing tool owns dismissal. -Synthetic poses come from `SnapshotConfiguration.Pose`; see the measurement provenance in [SnapshotKit](../../Shared/SnapshotKit/README.md). Native runtime geometry validation remains pending. +Locations owns an optional `CalendarMonthReveal` value. Month controls create a new +request identity even when the destination is unchanged. Calendar applies the request +once its content is ready and retains the consumed month for tabletop display. +Calendar detail scroll restoration remains authoritative; entering overview or +tabletop does not overwrite a saved forecast anchor. Timeline owns its typed row IDs +and restores browsing independently, without calendar-to-timeline selection machinery. + +Planning sheets remain above changing containers, with one owner per destination. +The welcome and developer overlays share the geometry adapter to find clear space. +The DEBUG **Duo Layout Lab** retains one production demo world while its pose control +changes bounds, traits, and synthetic regions. Embedded demo tools leave dismissal +to their containing tool. + +Synthetic poses come from `SnapshotConfiguration.DuoPose`; see the measurement provenance in [SnapshotKit](../../Shared/SnapshotKit/README.md). Native runtime geometry validation remains pending. Viewport shell captures supplement full-content captures of the shared scrolling panes. Views apply their initial position once the corresponding content is ready. Synthetic layouts do not validate system bars, camera activation, or physical folding. Those require native-device checks. diff --git a/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift index 715c3c54f..e9dd070b2 100644 --- a/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift @@ -11,7 +11,7 @@ struct LocationsViewSnapshotTests { @Test func adaptiveLoadStates() async throws { for state in AdaptivePaneLoadFixture.State.allCases { - for pose in [SnapshotConfiguration.Pose.innerPortrait, .tabletop] { + for pose in [SnapshotConfiguration.DuoPose.innerPortrait, .tabletop] { let fixture = try await AdaptivePaneLoadFixture(state: state) await assertSnapshots( of: LocationsView(report: fixture.report) diff --git a/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift index be7a6ac89..03ff12009 100644 --- a/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift @@ -11,7 +11,7 @@ struct YearViewSnapshotTests { @Test func adaptiveLoadStates() async throws { for state in AdaptivePaneLoadFixture.State.allCases { - for pose in [SnapshotConfiguration.Pose.innerPortrait, .tabletop] { + for pose in [SnapshotConfiguration.DuoPose.innerPortrait, .tabletop] { let fixture = try await AdaptivePaneLoadFixture(state: state) await assertSnapshots( of: YearView(report: fixture.report) diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.BrowseColumn_browseColumn.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.BrowseColumn_browseColumn.png deleted file mode 100644 index 05949d7d3..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.BrowseColumn_browseColumn.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c5b163541a3f3256235b843245989b64bac123aa3ff36e841e046aa87d6a82d3 -size 236048 diff --git a/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutLabView.swift b/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutLabView.swift index 83d8571b0..1e12d9d11 100644 --- a/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutLabView.swift +++ b/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutLabView.swift @@ -6,12 +6,12 @@ /// A DEBUG-only mounted production scene. Pose controls change geometry, never its models. struct DuoLayoutLabView: View { @State private var model: DuoLayoutLabModel - @State private var pose: SnapshotConfiguration.Pose + @State private var pose: SnapshotConfiguration.DuoPose private let initialRegion: Region? init( model: DuoLayoutLabModel = DuoLayoutLabModel(), - pose: SnapshotConfiguration.Pose = .outerPortrait, + pose: SnapshotConfiguration.DuoPose = .outerPortrait, initialRegion: Region? = nil, ) { _model = State(initialValue: model) @@ -26,7 +26,7 @@ Text(String(localized: .developerDuoTitle)) .font(.headline) Picker(String(localized: .developerDuoPose), selection: $pose) { - ForEach(SnapshotConfiguration.Pose.allCases) { pose in + ForEach(SnapshotConfiguration.DuoPose.allCases) { pose in Text(pose.title).tag(pose) } } @@ -94,7 +94,7 @@ extension DuoLayoutLabView: SnapshotProviding { static var snapshots: [SnapshotCase] { - [SnapshotConfiguration.Pose.outerPortrait, .tabletop].map { pose in + [SnapshotConfiguration.DuoPose.outerPortrait, .tabletop].map { pose in let fixture = DuoLayoutSnapshotFixture(tab: .locations, selection: .region) return whereSnapshot( name: pose.rawValue, diff --git a/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutSnapshotFixture.swift b/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutSnapshotFixture.swift index 14823ffca..54521fc6c 100644 --- a/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutSnapshotFixture.swift +++ b/Where/WhereUI/Sources/Developer/DuoLayout/DuoLayoutSnapshotFixture.swift @@ -70,7 +70,7 @@ struct DuoLayoutSnapshotScene: View { let fixture: DuoLayoutSnapshotFixture - let pose: SnapshotConfiguration.Pose + let pose: SnapshotConfiguration.DuoPose var body: some View { Group { diff --git a/Where/WhereUI/Sources/MainTabs.swift b/Where/WhereUI/Sources/MainTabs.swift index 2e6fc4269..e293b8f76 100644 --- a/Where/WhereUI/Sources/MainTabs.swift +++ b/Where/WhereUI/Sources/MainTabs.swift @@ -351,7 +351,7 @@ struct MainTabs: View { private static var adaptiveSnapshots: [SnapshotCase] { let tabs: [MainTabSelection.TabID] = [.locations, .year, .settings] var cases = tabs.flatMap { tab in - SnapshotConfiguration.Pose.allCases.map { pose in + SnapshotConfiguration.DuoPose.allCases.map { pose in adaptiveSnapshot( tab: tab, pose: pose, @@ -361,7 +361,7 @@ struct MainTabs: View { } } for tab in tabs { - let pose = SnapshotConfiguration.Pose.innerPortrait + let pose = SnapshotConfiguration.DuoPose.innerPortrait let configurations = [ SnapshotConfiguration(colorScheme: .dark, device: pose.frame), SnapshotConfiguration(dynamicType: .accessibility5, device: pose.frame), @@ -378,7 +378,7 @@ struct MainTabs: View { ) } } - for pose in [SnapshotConfiguration.Pose.innerPortrait, .tabletop, .book] { + for pose in [SnapshotConfiguration.DuoPose.innerPortrait, .tabletop, .book] { cases.append(adaptiveSnapshot( tab: .locations, pose: pose, @@ -403,7 +403,7 @@ struct MainTabs: View { private static func adaptiveSnapshot( tab: MainTabSelection.TabID, - pose: SnapshotConfiguration.Pose, + pose: SnapshotConfiguration.DuoPose, configuration: SnapshotConfiguration, selection: DuoLayoutSnapshotFixture.Selection, ) -> SnapshotCase { diff --git a/Where/WhereUI/Sources/Primary/CalendarContentView.swift b/Where/WhereUI/Sources/Primary/CalendarContentView.swift index 5b1e6db3d..4cc803523 100644 --- a/Where/WhereUI/Sources/Primary/CalendarContentView.swift +++ b/Where/WhereUI/Sources/Primary/CalendarContentView.swift @@ -13,7 +13,9 @@ import SwiftUI /// it — region-focused, with a title — as the zoom destination of a tapped card. struct CalendarContentView: View { enum Presentation: Equatable { case year, month } - let history: PresenceSelectionModel? + @Binding private var monthReveal: CalendarMonthReveal? + private let allowsMonthSelection: Bool + let displayMode: Presentation enum ScrollRole: Hashable { case standalone, locations, year } struct ScrollContext: Hashable { let region: Region?; let year: Int } @@ -41,7 +43,10 @@ struct CalendarContentView: View { @AccessibilityFocusState private var focusedTarget: CalendarPresentationState.Target? @State private var zoom = CalendarZoomTransitionState() - @BScrollPosition private var detailPosition: ScrollPosition + @BScrollPosition< + CalendarPresentationState.Target, + CalendarContentView.ScrollContext + > private var detailPosition: ScrollPosition @State private var overviewPosition = ScrollPosition( idType: CalendarPresentationState.Target.self, edge: .top, @@ -67,7 +72,7 @@ struct CalendarContentView: View { init( focusedRegion: Region? = nil, report: YearReportModel, - history: PresenceSelectionModel? = nil, + monthReveal: Binding? = nil, presentationState: CalendarPresentationState? = nil, presentation: Presentation = .year, initialMode: CalendarPresentationState.Mode = .detail, @@ -77,7 +82,8 @@ struct CalendarContentView: View { ) { self.focusedRegion = focusedRegion self.report = report - self.history = history + _monthReveal = monthReveal ?? .constant(nil) + allowsMonthSelection = monthReveal != nil suppliedPresentation = presentationState displayMode = presentation suppliedPlanningDestination = planningDestination @@ -346,7 +352,7 @@ struct CalendarContentView: View { ) } } - .onChange(of: history?.requestID, initial: true) { _ = applyReveal() } + .onChange(of: monthReveal?.id, initial: true) { _ = applyReveal() } .overlayPreferenceValue(CalendarZoomFrames.self) { anchors in Color.clear .onGeometryChange(for: CalendarZoomTransitionState.Measurement?.self) { proxy in @@ -378,7 +384,7 @@ struct CalendarContentView: View { @discardableResult private func applyReveal() -> Bool { - guard let history, let request = history.pendingRequest(for: .calendar), + guard let request = monthReveal, request.isPending, request.month.year == report.selectedYear, presentation.year == report.selectedYear, presentation.isAvailable(.month(request.month)) @@ -388,14 +394,23 @@ struct CalendarContentView: View { if displayMode == .year { _ = presentation.expand(target) } _detailPosition.scrollTo(id: target, anchor: .top) tabletopPosition.scrollTo(id: target, anchor: .top) - history.consume(request.id, by: .calendar) + monthReveal?.isPending = false return true } private var selectedMonth: CalendarDay? { - guard let day = history?.selectedInterval(in: report.selectedYear)?.lowerBound + guard let month = monthReveal?.month, month.year == report.selectedYear else { return nil } + return month + } + + private var selectedInterval: ClosedRange? { + guard let month = selectedMonth, + let interval = report.calendar.dateInterval( + of: .month, + for: month.startOfDay(in: report.calendar), + ) else { return nil } - return CalendarDay(year: day.year, month: day.month, day: 1) + return month ... CalendarDay(from: interval.end, in: report.calendar).adding(days: -1) } private func expand(_ target: CalendarPresentationState.Target) { @@ -515,9 +530,9 @@ struct CalendarContentView: View { compact: compact, isAvailable: isAvailable, displayMode: displayMode, - selectedInterval: history?.selectedInterval(in: report.selectedYear), - selectMonth: compact ? nil : history.map { history in - { history.select(month: monthKey(month), dateCalendar: report.calendar) } + selectedInterval: selectedInterval, + selectMonth: compact || !allowsMonthSelection ? nil : { + monthReveal = CalendarMonthReveal(month: monthKey(month)) }, ) } diff --git a/Where/WhereUI/Sources/Primary/CalendarMonthReveal.swift b/Where/WhereUI/Sources/Primary/CalendarMonthReveal.swift new file mode 100644 index 000000000..665ed91bc --- /dev/null +++ b/Where/WhereUI/Sources/Primary/CalendarMonthReveal.swift @@ -0,0 +1,15 @@ +import Foundation +import WhereCore + +/// An explicit month command. Repeating a destination creates a new request identity. +/// Keep the consumed request to display the selected month without replaying it on remount. +struct CalendarMonthReveal: Identifiable { + struct ID: Hashable { private let value = UUID() } + let id = ID() + let month: CalendarDay + var isPending = true + + init(month: CalendarDay) { + self.month = CalendarDay(year: month.year, month: month.month, day: 1) + } +} diff --git a/Where/WhereUI/Sources/Primary/LocationsCardsView.swift b/Where/WhereUI/Sources/Primary/LocationsCardsView.swift index 034dc365a..f8f7a7567 100644 --- a/Where/WhereUI/Sources/Primary/LocationsCardsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsCardsView.swift @@ -17,7 +17,7 @@ struct LocationsCardsView: View { let cardVariant: WhereStylesheet.CardStyle.Variant let showsSelection: Bool - @BScrollPosition private var position: ScrollPosition + @BScrollPosition private var position: ScrollPosition enum ScrollRole: Hashable { case browse } init( diff --git a/Where/WhereUI/Sources/Primary/LocationsView.swift b/Where/WhereUI/Sources/Primary/LocationsView.swift index 2393e066b..180b4682e 100644 --- a/Where/WhereUI/Sources/Primary/LocationsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsView.swift @@ -39,7 +39,7 @@ struct LocationsView: View { let report: YearReportModel var openElsewhere: (() -> Void)? @State private var navigation: LocationsView.Navigation - @State private var history = PresenceSelectionModel() + @State private var monthReveal: CalendarMonthReveal? @State private var calendarPresentation: CalendarPresentationState @State private var cardPresentation: LocationCardsPresentationModel @State private var tilt = TiltProvider() @@ -79,7 +79,7 @@ struct LocationsView: View { .safeAreaInset(edge: .top, spacing: 0) { MonthNavigationControls( report: report, - history: history, + monthReveal: $monthReveal, region: navigation.selectedRegion, scrollRole: .locations, ) @@ -128,13 +128,13 @@ struct LocationsView: View { destination: $navigation.planningDestination, )) .onChange(of: navigation.selectedRegion) { - history.reset() + monthReveal = nil calendarPresentation = CalendarPresentationState(mode: CalendarModel( report: report, persistenceKey: navigation.selectedRegion.map(CalendarZoomKey.region), ).isOverview(fallback: false) ? .overview : .detail) } - .onChange(of: report.selectedYear) { history.reset() } + .onChange(of: report.selectedYear) { monthReveal = nil } } private var browseContent: some View { @@ -168,7 +168,7 @@ struct LocationsView: View { CalendarContentView( focusedRegion: navigation.selectedRegion, report: report, - history: history, + monthReveal: $monthReveal, presentationState: calendarPresentation, presentation: presentation, persistenceKey: navigation.selectedRegion.map(CalendarZoomKey.region), diff --git a/Where/WhereUI/Sources/Primary/MonthNavigationControls.swift b/Where/WhereUI/Sources/Primary/MonthNavigationControls.swift index a6383b007..5efb64ea9 100644 --- a/Where/WhereUI/Sources/Primary/MonthNavigationControls.swift +++ b/Where/WhereUI/Sources/Primary/MonthNavigationControls.swift @@ -4,19 +4,22 @@ import SFSafeSymbols import SwiftUI import WhereCore -/// Explicit month browsing controls shared by both tabletop destinations. +/// Explicit month browsing controls for the Locations tabletop calendar. struct MonthNavigationControls: View { let report: YearReportModel - let history: PresenceSelectionModel - @BScrollPosition private var position: ScrollPosition + @Binding var monthReveal: CalendarMonthReveal? + @BScrollPosition< + CalendarPresentationState.Target, + CalendarContentView.ScrollContext + > private var position: ScrollPosition init( report: YearReportModel, - history: PresenceSelectionModel, + monthReveal: Binding, region: Region? = nil, scrollRole: CalendarContentView.ScrollRole = .year, ) { self.report = report - self.history = history + _monthReveal = monthReveal _position = BScrollPosition( CalendarContentView.self, role: scrollRole, @@ -29,7 +32,7 @@ struct MonthNavigationControls: View { @Environment(\.stylesheet) private var stylesheet private var month: CalendarDay { - if let day = history.selectedInterval(in: report.selectedYear)?.lowerBound ?? position + if let day = monthReveal?.month ?? position .viewID(type: CalendarPresentationState.Target.self)?.month { return CalendarDay(year: day.year, month: day.month, day: 1) @@ -80,9 +83,8 @@ struct MonthNavigationControls: View { } private func select(_ month: Int) { - history.select( + monthReveal = CalendarMonthReveal( month: CalendarDay(year: report.selectedYear, month: month, day: 1), - dateCalendar: report.calendar, ) } } @@ -91,7 +93,7 @@ struct MonthNavigationControls: View { #Preview { MonthNavigationControls( report: PreviewSupport.loadedYearReportModel(), - history: PresenceSelectionModel(), + monthReveal: .constant(nil), ) .whereBroadwayRoot() } diff --git a/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift b/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift index 59aa49d12..72015ccf1 100644 --- a/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift +++ b/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift @@ -15,27 +15,37 @@ import WhereCore struct PresenceTimelineList: View { enum Presentation { case full - case linked case excerpt } + enum RowID: Hashable { + case recorded(RegionStint.ID) + case flight(FlightAssessment.ID) + case planned(PlanningTimelineItem.ID) + + init(_ item: PresenceJourneyItem) { + self = switch item { + case let .stay(stint): .recorded(stint.id) + case let .flight(flight): .flight(flight.id) + } + } + } + let report: YearReportModel let presentation: Presentation private let suppliedPlanningDestination: Binding? @State private var localPlanningDestination: PlannedStaysDestination? - @BScrollPosition private var position: ScrollPosition + @BScrollPosition private var position: ScrollPosition enum ScrollRole: Hashable { case timeline } private var planningDestination: Binding { suppliedPlanningDestination ?? $localPlanningDestination } @Environment(\.stylesheet) private var stylesheet - @State private var history: PresenceSelectionModel init( report: YearReportModel, presentation: Presentation = .full, - history: PresenceSelectionModel? = nil, planningDestination: Binding? = nil, ) { self.report = report @@ -45,9 +55,8 @@ struct PresenceTimelineList: View { Self.self, role: ScrollRole.timeline, context: { report.selectedYear }, - idType: PresenceSelectionModel.Item.self, + idType: RowID.self, ) - _history = State(initialValue: history ?? PresenceSelectionModel()) } var body: some View { @@ -67,7 +76,7 @@ struct PresenceTimelineList: View { today: report.forecasts.today, ) : [] - let rowIDs = recordedItems.map(PresenceSelectionModel.Item.init) + let rowIDs = recordedItems.map(RowID.init) + plannedItems.map { .planned($0.id) } Group { @@ -148,7 +157,7 @@ struct PresenceTimelineList: View { .padding(.horizontal, stylesheet.spacing.xxLarge) .padding( .top, - pinsOverview || presentation == .linked ? 0 : stylesheet.spacing.large, + pinsOverview ? 0 : stylesheet.spacing.large, ) .padding(.bottom, stylesheet.spacing.large) } @@ -157,34 +166,16 @@ struct PresenceTimelineList: View { year: report.selectedYear, recordedItems: recordedItems, plannedItems: plannedItems, - request: history.requestID, )) { guard !Task.isCancelled else { return } - let initial = presentation == .linked ? recordedItems.last - .map(PresenceSelectionModel.Item.init) : nil - if let initial { _position.initialize(id: initial, anchor: .top) } - else { _position.initialize(edge: .bottom) } - if let target = position.viewID(type: PresenceSelectionModel.Item.self), + _position.initialize(edge: .bottom) + if let target = position.viewID(type: RowID.self), !rowIDs.contains(target) { - if let initial { _position.scrollTo(id: initial, anchor: .top) } - else { _position.scrollTo(edge: .bottom) } - } - if let request = history.pendingRequest(for: .timeline), - let target = history.timelineTarget( - recordedItems: recordedItems, - plannedItems: plannedItems, - calendar: report.calendar, - ) - { - _position.scrollTo(id: target, anchor: .top) - history.consume(request.id, by: .timeline) + _position.scrollTo(edge: .bottom) } } - .defaultScrollAnchor( - presentation == .linked ? .top : .bottom, - for: .initialOffset, - ) + .defaultScrollAnchor(.bottom, for: .initialOffset) .id(report.selectedYear) } } @@ -212,7 +203,6 @@ struct PresenceTimelineList: View { let year: Int let recordedItems: [PresenceJourneyItem] let plannedItems: [PlanningTimelineItem] - let request: PresenceSelectionModel.Request.ID? } /// Both presentations share recorded and planned row rendering. @@ -239,20 +229,7 @@ struct PresenceTimelineList: View { cardPosition: joinsRecorded && item.id == recordedItems.last?.id ? .top : .standalone, ) - if presentation == .excerpt { - row - } else { - Button { - history.select(stint: stint, dateCalendar: report.calendar) - } label: { - row.presenceSelection(isSelected( - start: CalendarDay(from: stint.start, in: report.calendar), - end: CalendarDay(from: stint.end, in: report.calendar), - region: stint.region, - )) - } - .buttonStyle(.plain) - } + row case let .flight(flight): FlightJourneyRow( flight: flight, @@ -263,13 +240,7 @@ struct PresenceTimelineList: View { } } .fixedSize(horizontal: false, vertical: presentation == .excerpt) - // Include the content's leading gap in its first semantic target. - .padding( - .top, - presentation == .linked && item.id == recordedItems.first?.id - ? stylesheet.spacing.large : 0, - ) - .id(PresenceSelectionModel.Item(item)) + .id(RowID(item)) } ForEach(plannedItems) { item in let row = PlannedPresenceJourneyRow( @@ -285,14 +256,9 @@ struct PresenceTimelineList: View { if presentation != .excerpt { Button { - history.select(plannedItem: item) - if presentation == .full { edit(item) } + edit(item) } label: { - row.presenceSelection(isSelected( - start: item.start, - end: item.end, - region: item.region, - )) + row } .buttonStyle(.plain) .contextMenu { @@ -303,12 +269,7 @@ struct PresenceTimelineList: View { edit(item) } } - .padding( - .top, - presentation == .linked && recordedItems.isEmpty - && item.id == plannedItems.first?.id ? stylesheet.spacing.large : 0, - ) - .id(PresenceSelectionModel.Item.planned(item.id)) + .id(RowID.planned(item.id)) } else { row } @@ -325,13 +286,6 @@ struct PresenceTimelineList: View { } } - private func isSelected(start: CalendarDay, end: CalendarDay, region: Region) -> Bool { - guard let selection = history.selection, - selection.region == nil || selection.region == region - else { return false } - return selection.interval.overlaps(start ... end) - } - private var showsForecast: Bool { report.showsEstimatedTimeAndPlanning && !timelineForecasts.isEmpty } @@ -356,18 +310,6 @@ struct PresenceTimelineList: View { .interruptedStayYearReportModel()) } }, - whereSnapshot( - name: "BrowseColumn", - configurations: [SnapshotConfiguration(device: DuoLayoutSnapshotFixture - .browseColumn)], - ) { - NavigationStack { - PresenceTimelineList( - report: PreviewSupport.loadedYearReportModel(), - presentation: .linked, - ) - } - }, whereSnapshot(name: "Flights", configurations: .fullContentScreenRepresentative) { NavigationStack { PresenceTimelineList(report: PreviewSupport diff --git a/Where/WhereUI/Tests/CalendarContentViewTests.swift b/Where/WhereUI/Tests/CalendarContentViewTests.swift index 9d8b71d09..32d204289 100644 --- a/Where/WhereUI/Tests/CalendarContentViewTests.swift +++ b/Where/WhereUI/Tests/CalendarContentViewTests.swift @@ -103,11 +103,11 @@ struct CalendarContentViewTests { @Test func overviewAndTabletopDoNotReplaceTheForecastAnchor() async throws { let report = PreviewSupport.loadedYearReportModel() let fixture = CalendarPresentationFixture() - let history = PresenceSelectionModel() + let reveals = CalendarRevealTestSupport() let host = UIHostingController(rootView: CalendarPresentationHarness( fixture: fixture, report: report, - history: history, + reveals: reveals, )) try await show(host) { controller in controller.view.window?.layer.speed = 1 @@ -281,23 +281,23 @@ struct CalendarContentViewTests { @Test func aRevealWaitsForDelayedCalendarData() async throws { let store = try TestStore() let report = PlanningTestSupport.report(store: store) - let history = PresenceSelectionModel() + let reveals = CalendarRevealTestSupport() let month = CalendarDay(year: report.selectedYear, month: 2, day: 1) - history.select(month: month, dateCalendar: report.calendar) + reveals.select(month: month) await store.enableFirstSamplesGate() let load = Task { await report.refresh() } await store.awaitFirstSamplesCall() let host = UIHostingController(rootView: CalendarContentView( report: report, - history: history, + monthReveal: reveals.binding, ).whereBroadwayRoot().bScrollRestorationScope()) do { try await show(host) { _ in - #expect(history.pendingRequest(for: .calendar) != nil) + #expect(reveals.request?.isPending == true) await store.releaseFirstSamplesCall() await load.value try await waitForCalendar("delayed February reveal") { - history.pendingRequest(for: .calendar) == nil && isVisible( + reveals.request?.isPending == false && isVisible( "February", in: host.view, expanded: true, @@ -311,14 +311,14 @@ struct CalendarContentViewTests { } } - @Test func repeatedLinkedSelectionExpandsOverviewAndKeepsMonthTarget() async throws { + @Test func repeatedMonthRevealExpandsOverviewAndKeepsMonthTarget() async throws { let report = PreviewSupport.loadedYearReportModel() - let history = PresenceSelectionModel() + let reveals = CalendarRevealTestSupport() let month = CalendarDay(year: report.selectedYear, month: 2, day: 1) let host = UIHostingController(rootView: NavigationStack { CalendarContentView( report: report, - history: history, + monthReveal: reveals.binding, initialMode: .overview, persistenceKey: .year, ) @@ -327,9 +327,9 @@ struct CalendarContentViewTests { try await waitForCalendar("overview to load") { isEnabled("Show Months", in: host.view) } for _ in 0 ..< 2 { - history.select(month: month, dateCalendar: report.calendar) + reveals.select(month: month) try await waitForCalendar("explicit February reveal") { - history.pendingRequest(for: .calendar) == nil + reveals.request?.isPending == false && isVisible("February", in: host.view, expanded: true) && isEnabled("Show Year", in: host.view) } @@ -342,21 +342,76 @@ struct CalendarContentViewTests { } } + @Test func aConsumedRevealDoesNotReplayOverBrowsingAfterRemount() async throws { + let report = PreviewSupport.loadedYearReportModel() + let reveals = CalendarRevealTestSupport() + let capture = ScrollPositionCapture() + let store = BScrollRestorationStore() + let february = CalendarDay(year: report.selectedYear, month: 2, day: 1) + let june = CalendarPresentationState.Target.month(CalendarDay( + year: report.selectedYear, + month: 6, + day: 1, + )) + func calendar() -> some View { + CalendarContentView(report: report, monthReveal: reveals.binding) + .overlay { + ScrollPositionProbe( + CalendarContentView.self, + role: CalendarContentView.ScrollRole.standalone, + context: CalendarContentView.ScrollContext( + region: nil, + year: report.selectedYear, + ), + idType: CalendarPresentationState.Target.self, + capture: capture, + ) + } + .whereBroadwayRoot() + .bScrollRestorationScope(store) + } + let first = UIHostingController(rootView: calendar()) + try await show(first) { _ in + reveals.select(month: february) + try await waitForCalendar("February reveal consumed") { + reveals.request?.isPending == false && isVisible( + "February", + in: first.view, + expanded: true, + ) + } + capture.binding?.wrappedValue.scrollTo(id: june, anchor: .top) + try await waitForCalendar("independent browsing to June") { + isVisible("June", in: first.view, expanded: true) + } + } + let requestID = try #require(reveals.request?.id) + let second = UIHostingController(rootView: calendar()) + try await show(second) { _ in + try await waitForCalendar("June restored without replaying February") { + capture.binding?.wrappedValue + .viewID(type: CalendarPresentationState.Target.self) == june + && isVisible("June", in: second.view, expanded: true) + } + #expect(reveals.request?.id == requestID) + #expect(reveals.request?.isPending == false) + } + } + @Test func mountedCalendarPositionsAfterTheNewYearsMonthsArrive() async throws { let report = PreviewSupport.loadedYearReportModel() - let history = PresenceSelectionModel() + let reveals = CalendarRevealTestSupport() let host = UIHostingController(rootView: CalendarContentView( report: report, - history: history, + monthReveal: reveals.binding, ) .whereBroadwayRoot().bScrollRestorationScope()) try await show(host) { _ in try await waitForCalendar("initial forecast") { element("Planned stays", in: host.view) != nil } - history.select( + reveals.select( month: CalendarDay(year: report.selectedYear, month: 2, day: 1), - dateCalendar: report.calendar, ) try await waitForCalendar("February before year change") { isVisible( "February", @@ -403,11 +458,11 @@ struct CalendarContentViewTests { @Test func aRemountedCalendarUsesItsRenderedMonthAnchor() async throws { let report = PreviewSupport.loadedYearReportModel() - let history = PresenceSelectionModel() + let reveals = CalendarRevealTestSupport() let store = BScrollRestorationStore() let first = UIHostingController(rootView: CalendarContentView( report: report, - history: history, + monthReveal: reveals.binding, ) .whereBroadwayRoot().bScrollRestorationScope(store)) try await show(first) { _ in @@ -415,9 +470,8 @@ struct CalendarContentViewTests { "Planned stays", in: first.view, ) != nil } - history.select( + reveals.select( month: CalendarDay(year: report.selectedYear, month: 2, day: 1), - dateCalendar: report.calendar, ) try await waitForCalendar("rendered February anchor") { isVisible( "February", @@ -425,7 +479,7 @@ struct CalendarContentViewTests { expanded: true, ) } } - // A new coordinator has no reveal request: only the retained native position can restore + // A new view has no reveal request: only the retained native position can restore // February. let second = UIHostingController(rootView: CalendarContentView(report: report) .whereBroadwayRoot().bScrollRestorationScope(store)) diff --git a/Where/WhereUI/Tests/CalendarRevealTestSupport.swift b/Where/WhereUI/Tests/CalendarRevealTestSupport.swift new file mode 100644 index 000000000..c56343584 --- /dev/null +++ b/Where/WhereUI/Tests/CalendarRevealTestSupport.swift @@ -0,0 +1,18 @@ +import Observation +import SwiftUI +import WhereCore +@testable import WhereUI + +@MainActor @Observable +final class CalendarRevealTestSupport { + var request: CalendarMonthReveal? + + var binding: Binding { + @Bindable var owner = self + return $owner.request + } + + func select(month: CalendarDay) { + request = CalendarMonthReveal(month: month) + } +} diff --git a/Where/WhereUI/Tests/LocationsViewTests.swift b/Where/WhereUI/Tests/LocationsViewTests.swift index 2806231b4..58036d284 100644 --- a/Where/WhereUI/Tests/LocationsViewTests.swift +++ b/Where/WhereUI/Tests/LocationsViewTests.swift @@ -32,7 +32,7 @@ struct LocationsViewTestsDuo { // Returning to browsing retains the selected region for the next unfold. navigation.compactColumn = .sidebar for pose in [ - SnapshotConfiguration.Pose.outerPortrait, + SnapshotConfiguration.DuoPose.outerPortrait, .innerLandscape, .tabletop, .outerPortrait, @@ -184,7 +184,7 @@ struct LocationsViewTestsDuo { .compactMap { $0 as? UINavigationController } .first { $0.viewControllers.count > 1 }) let pushed = try #require(stack.topViewController) - for pose in [SnapshotConfiguration.Pose.innerPortrait, .tabletop, .outerPortrait] { + for pose in [SnapshotConfiguration.DuoPose.innerPortrait, .tabletop, .outerPortrait] { try await AdaptivePaneTestSupport.resize(controller, state: state, to: pose) #expect(navigation.browsePath.count == 1) #expect(controllers(controller).contains { $0 === pushed }) @@ -225,7 +225,7 @@ struct LocationsViewTestsDuo { try await show(host) { controller in let readsBeforeFolding = await store.sampleReadCount for pose in [ - SnapshotConfiguration.Pose.outerPortrait, + SnapshotConfiguration.DuoPose.outerPortrait, .innerPortrait, .tabletop, .outerPortrait, @@ -236,7 +236,11 @@ struct LocationsViewTestsDuo { } await report.select(year: 2026) let readsAfterYearChange = await store.sampleReadCount - for pose in [SnapshotConfiguration.Pose.innerLandscape, .tabletop, .outerPortrait] { + for pose in [ + SnapshotConfiguration.DuoPose.innerLandscape, + .tabletop, + .outerPortrait, + ] { try await AdaptivePaneTestSupport.resize(controller, state: state, to: pose) #expect(await store.sampleReadCount == readsAfterYearChange) } diff --git a/Where/WhereUI/Tests/MainTabsTests.swift b/Where/WhereUI/Tests/MainTabsTests.swift index 5acec8077..5fd723bcc 100644 --- a/Where/WhereUI/Tests/MainTabsTests.swift +++ b/Where/WhereUI/Tests/MainTabsTests.swift @@ -90,7 +90,7 @@ struct MainTabsTestsDuo { try await show(host) { controller in var enteredQuery = false for pose in [ - SnapshotConfiguration.Pose.outerPortrait, + SnapshotConfiguration.DuoPose.outerPortrait, .innerPortrait, .tabletop, .book, @@ -167,7 +167,7 @@ struct MainTabsTestsDuo { picker.date = draft picker.sendActions(for: .valueChanged) for pose in [ - SnapshotConfiguration.Pose.innerPortrait, + SnapshotConfiguration.DuoPose.innerPortrait, .tabletop, .outerPortrait, .innerLandscape, diff --git a/Where/WhereUI/Tests/PlannedStayEditorTests.swift b/Where/WhereUI/Tests/PlannedStayEditorTests.swift index cb62bc87c..4bdb15b2b 100644 --- a/Where/WhereUI/Tests/PlannedStayEditorTests.swift +++ b/Where/WhereUI/Tests/PlannedStayEditorTests.swift @@ -26,7 +26,7 @@ struct PlannedStayEditorTests { await gate.waitUntilReached() do { for pose in [ - SnapshotConfiguration.Pose.outerPortrait, + SnapshotConfiguration.DuoPose.outerPortrait, .innerPortrait, .tabletop, .outerPortrait, diff --git a/Where/WhereUI/Tests/PresenceTimelineListTests.swift b/Where/WhereUI/Tests/PresenceTimelineListTests.swift index 75e2a8b66..aeda2f638 100644 --- a/Where/WhereUI/Tests/PresenceTimelineListTests.swift +++ b/Where/WhereUI/Tests/PresenceTimelineListTests.swift @@ -8,134 +8,102 @@ import WhereCore @MainActor struct PresenceTimelineListTests { - @Test func aPendingRevealRetriesWhenAStayExtendsWithoutChangingIdentity() async throws { - let store = try TestStore() - let report = PlanningTestSupport.report(store: store) - for day in 20 ... 31 { - try await report.services.journal.addManualDay( - date: CalendarDay(year: report.selectedYear, month: 1, day: day) - .startOfDay(in: report.calendar), - regions: [.california], - audit: nil, - ) - } - await report.refresh() - let before = try PresenceTimeline.stints( - from: #require(report.report), - calendar: report.calendar, - ) - let original = try #require(before.first) - let history = PresenceSelectionModel() - history.select( - month: CalendarDay(year: report.selectedYear, month: 2, day: 1), - dateCalendar: report.calendar, - ) - let host = UIHostingController(rootView: PresenceTimelineList( - report: report, - presentation: .linked, - history: history, - ).whereBroadwayRoot().bScrollRestorationScope()) - try await show(host) { _ in - #expect(history.pendingRequest(for: .timeline) != nil) - for day in 1 ... 3 { - try await report.services.journal.addManualDay( - date: CalendarDay(year: report.selectedYear, month: 2, day: day) - .startOfDay(in: report.calendar), - regions: [.california], - audit: nil, - ) - } - await report.refresh() - let updatedReport = try #require(report.report) - let extended = try #require(PresenceTimeline.stints( - from: updatedReport, - calendar: report.calendar, - ).first) - #expect(extended.id == original.id) - try await waitForAsync { history.pendingRequest(for: .timeline) == nil } - } - } - - @Test func selectingTheSameMonthAgainRevealsItAfterIndependentBrowsing() async throws { + @Test func aRemountedTimelineRestoresItsBrowsedRowWithoutSelectionState() async throws { let world = try await WhereFlyoverWorld.build() let report = try #require(world.report.report) - let history = PresenceSelectionModel() - let month = CalendarDay(year: report.year, month: 5, day: 1) - let end = CalendarDay(year: report.year, month: 5, day: 31) - let stints = PresenceTimeline.stints(from: report, calendar: world.report.calendar) let items = PresenceJourneyItem.items( - stints: stints, + stints: PresenceTimeline.stints(from: report, calendar: world.report.calendar), flights: world.report.completedFlights, year: report.year, calendar: world.report.calendar, ) - let displayedStints = items.compactMap { item -> RegionStint? in + let stint = try #require(items.compactMap { item -> RegionStint? in if case let .stay(stint) = item { return stint } return nil - } - let expected = try #require(displayedStints.first { - CalendarDay(from: $0.start, in: world.report.calendar) <= end - && CalendarDay(from: $0.end, in: world.report.calendar) >= month - }) - let browsed = try #require(displayedStints.last) - #expect(expected.id != browsed.id) - let expectedLabel = WhereFormat.timelineRowAccessibility( - region: expected.region.localizedName, - range: DateRangeFormatting - .abbreviated( - start: expected.start, - end: expected.end, - calendar: world.report.calendar, - ), - days: expected.dayCount, - ) - let browsedLabel = WhereFormat.timelineRowAccessibility( - region: browsed.region.localizedName, - range: DateRangeFormatting - .abbreviated( - start: browsed.start, - end: browsed.end, - calendar: world.report.calendar, - ), - days: browsed.dayCount, + }.first) + let target = PresenceTimelineList.RowID.recorded(stint.id) + let label = WhereFormat.timelineRowAccessibility( + region: stint.region.localizedName, + range: DateRangeFormatting.abbreviated( + start: stint.start, + end: stint.end, + calendar: world.report.calendar, + ), + days: stint.dayCount, ) + let store = BScrollRestorationStore() let capture = ScrollPositionCapture() - let host = UIHostingController(rootView: PresenceTimelineList( - report: world.report, - presentation: .linked, - history: history, - ).overlay { - ScrollPositionProbe( - PresenceTimelineList.self, - role: PresenceTimelineList.ScrollRole.timeline, - context: report.year, - idType: PresenceSelectionModel.Item.self, - capture: capture, - ) - }.whereBroadwayRoot().bScrollRestorationScope()) - - try await show(host) { _ in - for _ in 0 ..< 4 { - history.select(month: month, dateCalendar: world.report.calendar) - try await waitForAsync { - capture.binding?.wrappedValue - .viewID(type: PresenceSelectionModel.Item.self) == .recorded(expected.id) - && HostedAccessibility.isVisible(expectedLabel, in: host.view) + func timeline() -> some View { + PresenceTimelineList(report: world.report) + .overlay { + ScrollPositionProbe( + PresenceTimelineList.self, + role: PresenceTimelineList.ScrollRole.timeline, + context: report.year, + idType: PresenceTimelineList.RowID.self, + capture: capture, + ) } - #expect(history.pendingRequest(for: .timeline) == nil) - let selectedInterval = history.selection - capture.binding?.wrappedValue.scrollTo( - id: PresenceSelectionModel.Item.recorded(browsed.id), - anchor: .top, + .whereBroadwayRoot() + .bScrollRestorationScope(store) + } + func waitForTimeline( + _ stage: String, + in view: UIView, + until condition: () -> Bool, + ) async throws { + do { + try await waitForAsync { condition() } + } catch { + let scrolls = AdaptivePaneTestSupport.descendants( + of: view, + matching: UIScrollView.self, ) - try await waitForAsync { - capture.binding?.wrappedValue - .viewID(type: PresenceSelectionModel.Item.self) == .recorded(browsed.id) - && HostedAccessibility.isVisible(browsedLabel, in: host.view) + let geometry = scrolls.map { + "offset=\($0.contentOffset), size=\($0.contentSize), bounds=\($0.bounds), insets=\($0.adjustedContentInset)" } - #expect(capture.binding?.wrappedValue - .viewID(type: PresenceSelectionModel.Item.self) == .recorded(browsed.id)) - #expect(history.selection == selectedInterval) + throw TestHostError( + "Timeline \(stage): \(error); target=\(String(describing: capture.binding?.wrappedValue.viewID(type: PresenceTimelineList.RowID.self))); \(geometry)", + ) + } + } + let first = UIHostingController(rootView: timeline()) + try await show(first) { _ in + // Native scrolling can clear ScrollPosition.edge after reaching the bottom. + // Wait for the rendered offset, not for the transient command to remain stored. + try await waitForTimeline("initial bottom", in: first.view) { + guard capture.binding != nil, + let scroll = AdaptivePaneTestSupport.descendants( + of: first.view, + matching: UIScrollView.self, + ) + .first(where: { $0.contentSize.height > $0.bounds.height }) + else { return false } + let bottom = scroll.contentSize.height - scroll.bounds.height + scroll + .adjustedContentInset.bottom + return abs(scroll.contentOffset.y - bottom) < 1 + } + capture.binding?.wrappedValue.scrollTo(id: target, anchor: .top) + try await waitForTimeline("browsed row", in: first.view) { + HostedAccessibility.isVisible( + label, + in: first.view, + ) + } + #expect(capture.binding?.wrappedValue + .viewID(type: PresenceTimelineList.RowID.self) == target) + let row = try #require(HostedAccessibility.objects(in: first.view).first { + $0.accessibilityLabel == label + }) + #expect(row.accessibilityTraits.contains(.button) == false) + #expect(row.accessibilityTraits.contains(.selected) == false) + } + let second = UIHostingController(rootView: timeline()) + try await show(second) { _ in + try await waitForTimeline("restored row", in: second.view) { + capture.binding?.wrappedValue + .viewID(type: PresenceTimelineList.RowID.self) == target + && HostedAccessibility.isVisible(label, in: second.view) } } } diff --git a/Where/WhereUI/Tests/ScrollPositionTestSupport.swift b/Where/WhereUI/Tests/ScrollPositionTestSupport.swift index b9f3b789c..f8d9eec8e 100644 --- a/Where/WhereUI/Tests/ScrollPositionTestSupport.swift +++ b/Where/WhereUI/Tests/ScrollPositionTestSupport.swift @@ -7,14 +7,14 @@ final class ScrollPositionCapture { var binding: Binding? } -struct ScrollPositionProbe: View { +struct ScrollPositionProbe: View { let capture: ScrollPositionCapture - @BScrollPosition private var position: ScrollPosition + @BScrollPosition private var position: ScrollPosition init( _ view: (some View).Type, role: some Hashable, - context: some Hashable, + context: Context, idType: ID.Type, capture: ScrollPositionCapture, ) { diff --git a/Where/WhereUI/Tests/Support/CalendarPresentationFixture.swift b/Where/WhereUI/Tests/Support/CalendarPresentationFixture.swift index 84cb32e40..71dac4162 100644 --- a/Where/WhereUI/Tests/Support/CalendarPresentationFixture.swift +++ b/Where/WhereUI/Tests/Support/CalendarPresentationFixture.swift @@ -15,7 +15,7 @@ final class CalendarPresentationFixture { struct CalendarPresentationHarness: View { let fixture: CalendarPresentationFixture let report: YearReportModel - let history: PresenceSelectionModel + let reveals: CalendarRevealTestSupport var body: some View { NavigationStack { @@ -24,7 +24,7 @@ struct CalendarPresentationHarness: View { CalendarContentView( focusedRegion: fixture.region, report: report, - history: history, + monthReveal: reveals.binding, presentation: .month, persistenceKey: fixture.region.map(CalendarZoomKey.region), ) @@ -32,7 +32,7 @@ struct CalendarPresentationHarness: View { CalendarContentView( focusedRegion: fixture.region, report: report, - history: history, + monthReveal: reveals.binding, persistenceKey: fixture.region.map(CalendarZoomKey.region), ) } diff --git a/Where/WhereUI/Tests/YearViewTests.swift b/Where/WhereUI/Tests/YearViewTests.swift index 640c6ec9b..b6f0130b6 100644 --- a/Where/WhereUI/Tests/YearViewTests.swift +++ b/Where/WhereUI/Tests/YearViewTests.swift @@ -18,7 +18,7 @@ struct YearViewTests { try await show(host) { controller in controller.view.window?.layer.speed = 1 var retainedMode = YearMode.calendar - for pose in SnapshotConfiguration.Pose.allCases + [.outerPortrait] { + for pose in SnapshotConfiguration.DuoPose.allCases + [.outerPortrait] { try await AdaptivePaneTestSupport.resize(controller, state: state, to: pose) try await waitForAsync { button(retainedMode, in: controller.view)?.accessibilityTraits From 0790712c1cdb43480a7c57ea5e23175bb02698a8 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 12 Nov 2026 16:47:32 -0500 Subject: [PATCH 17/18] Let Settings use native split navigation without fold geometry --- Where/WhereUI/README.md | 3 +- .../Sources/Settings/SettingsView.swift | 72 ++++++++++--------- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index bd544a22e..2b9109155 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -109,6 +109,8 @@ once its content is ready and retains the consumed month for tabletop display. Calendar detail scroll restoration remains authoritative; entering overview or tabletop does not overwrite a saved forecast anchor. Timeline owns its typed row IDs and restores browsing independently, without calendar-to-timeline selection machinery. +Settings uses native split navigation and the stylesheet's compact-layout policy; +it does not consult Locations' tabletop geometry. Planning sheets remain above changing containers, with one owner per destination. The welcome and developer overlays share the geometry adapter to find clear space. @@ -752,4 +754,3 @@ also avoids blank trailing space from lazy estimates in its bottom-anchored scro Developer Overlay snapshots retain their scene for native glass rendering. Detached windows produce material bands and clipping in the menu. The retained-scene cases keep their device traits, content clearance, and full-content sizing. - diff --git a/Where/WhereUI/Sources/Settings/SettingsView.swift b/Where/WhereUI/Sources/Settings/SettingsView.swift index db96cf555..0b56d3eb0 100644 --- a/Where/WhereUI/Sources/Settings/SettingsView.swift +++ b/Where/WhereUI/Sources/Settings/SettingsView.swift @@ -18,7 +18,7 @@ import WhereCore /// /// The top level owns nothing but navigation; behavior lives in the sub-screens /// (`DevicesSettingsView`, `AlertsSettingsView`, …). The scene injects its report. -/// This view owns the backup and reminders models above the adaptive container; +/// This view owns the backup and reminders models above the native navigation container; /// the `WhereSession` coordinator (recording/location) and /// `WhereModel` (reset) come from the environment via the sub-screens. struct SettingsView: View { @@ -74,45 +74,47 @@ struct SettingsView: View { var body: some View { @Bindable var navigation = navigation - AdaptivePaneContainer { arrangement in - NavigationSplitView( - columnVisibility: .constant(.all), - preferredCompactColumn: $navigation.compactColumn, - ) { - SettingsContent( - report: report, - recordingWarning: recordingWarning, - navigation: navigation, - ) - .navigationSplitViewColumnWidth( - min: stylesheet.adaptivePane.minimumControlsWidth, - ideal: stylesheet.adaptivePane.minimumDetailWidth, - ) - } detail: { - NavigationStack(path: $navigation.path) { - Group { - if let route = navigation.selectedRoute { - destination(for: route) - } else { - ContentUnavailableView( - String(localized: .settingsTitle), - systemSymbol: .gearshape, - ) - } - } - .navigationDestination(for: SettingsRoute.self) { route in + NavigationSplitView( + columnVisibility: .constant(.all), + preferredCompactColumn: $navigation.compactColumn, + ) { + SettingsContent( + report: report, + recordingWarning: recordingWarning, + navigation: navigation, + ) + .navigationSplitViewColumnWidth( + min: stylesheet.adaptivePane.minimumControlsWidth, + ideal: stylesheet.adaptivePane.minimumDetailWidth, + ) + } detail: { + NavigationStack(path: $navigation.path) { + Group { + if let route = navigation.selectedRoute { destination(for: route) + } else { + ContentUnavailableView( + String(localized: .settingsTitle), + systemSymbol: .gearshape, + ) } } - .navigationSplitViewColumnWidth( - min: stylesheet.adaptivePane.minimumDetailWidth, - ideal: stylesheet.adaptivePane.minimumDetailWidth, - ) + .navigationDestination(for: SettingsRoute.self) { route in + destination(for: route) + } } - .navigationSplitViewStyle(.balanced) - .environment(\.horizontalSizeClass, arrangement == .compact ? .compact : .regular) - .bHorizontalSizeClass(arrangement == .compact ? .compact : .regular) + .navigationSplitViewColumnWidth( + min: stylesheet.adaptivePane.minimumDetailWidth, + ideal: stylesheet.adaptivePane.minimumDetailWidth, + ) } + .navigationSplitViewStyle(.balanced) + .environment( + \.horizontalSizeClass, + stylesheet.adaptivePane.panes == .single ? .compact : .regular, + ) + .bHorizontalSizeClass(stylesheet.adaptivePane.panes == .single ? .compact : .regular) + .background(.background) .sheet(isPresented: $navigation.showRegions) { RegionsSettingsView(usedThisYear: regionsUsedThisYear) } From ca09946122b6a23743f78fad51b3ca75d4bea87e Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 12 Nov 2026 16:58:26 -0500 Subject: [PATCH 18/18] Update timeline annotations for passive recorded stays --- .../presenceTimeline.Flights_iPad_accessibility.png | 4 ++-- .../presenceTimeline.Flights_iPhone_accessibility.png | 4 ++-- ...presenceTimeline.UncertainArrival_iPhone_accessibility.png | 4 ++-- .../presenceTimeline.WithData_iPad_accessibility.png | 4 ++-- .../presenceTimeline.WithData_iPhone_accessibility.png | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad_accessibility.png index 69a4d051f..16fbc24a5 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8a02611fc35561b036cfb5ec9f00d4fe2017705868d276c809892565edc79aa -size 3329038 +oid sha256:8fe773648053a7ec40dd4ef4f104d57ecc3e56d001ff6ba5674bc971c2442896 +size 3318068 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone_accessibility.png index f4f0e5968..7d88d1209 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.Flights_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48f058f9857915608f3d39dc6d8530b3b418732602ecd8e0331217731a65e44f -size 2038737 +oid sha256:96d2f6954497d42f0bb5abddbe3691d7bdeb896d90e8dcc176b51d5d4c38c1ab +size 2027499 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.UncertainArrival_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.UncertainArrival_iPhone_accessibility.png index 97b1cc31e..4450a0622 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.UncertainArrival_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.UncertainArrival_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcf630a11325bcbffdc6476055075216603c353a7bc333a34b87fadf0d584e8f -size 1983786 +oid sha256:ea10c5dd2e9d24f47cc420b1e1b442ac601ab8fa69f641559fe54fd7bee2e5ec +size 1974410 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad_accessibility.png index 40e227670..9dd6bef63 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f831d6e25691d9d995772987842d58a4060928c1187dc101522da4b33c91e926 -size 3019263 +oid sha256:01bedc713c743764f1f32a0374169f8e807f2fd85ad911f5c8d5331d0b443943 +size 3008657 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone_accessibility.png index 9b5d4a267..cce59c08d 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PresenceTimelineListSnapshotTests/presenceTimeline.WithData_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c640130fe683dc621dd2df25d23ad6b823aab31ee99d9c020f9ca49230eb9a0a -size 1816444 +oid sha256:146ac8f0625a03a7eee1656addc0b511e5b2fc0fc6458cc83b4328aecf291c17 +size 1806812