Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
eecc0f5
Integrate adaptive Where destinations and Duo lab
kyleve Oct 16, 2026
2f7e92b
Preserve calendar zoom and accessibility across Duo layouts
kyleve Oct 30, 2026
ff3c921
Capture update settings through the shared scrolling content
kyleve Oct 30, 2026
57e1ac7
Refresh Duo integration snapshots after merging main
kyleve Oct 30, 2026
564e1be
Preserve initial estimate alignment across calendar remounts
kyleve Oct 30, 2026
ab11602
Save reviewed Duo capture updates and remaining validation gaps
kyleve Oct 31, 2026
fe85368
Read current window insets when delivering developer overlay clearance
kyleve Nov 9, 2026
e1a4e1d
Preserve Duo lab navigation with scene-backed pose previews
kyleve Nov 9, 2026
dd5e30e
Keep adaptive capture fixes with the app integration
kyleve Nov 11, 2026
f2bf635
Keep Duo presentation and restoration in stable view owners
kyleve Nov 11, 2026
b52d45f
Pin the lab forecast to its initial bottom alignment
kyleve Nov 11, 2026
dc5d7b0
Keep linked timeline padding inside its first scroll target
kyleve Nov 11, 2026
ebefbba
Carry pane and presence names through Duo consumers and lab
kyleve Nov 12, 2026
99295e1
Limit region selection indicators to companion-pane layouts
kyleve Nov 12, 2026
5412e31
Restore the standard Your Year selector at every size
kyleve Nov 12, 2026
4c0cd0a
Keep month reveals in Locations and timeline identity in its view
kyleve Nov 12, 2026
0790712
Let Settings use native split navigation without fold geometry
kyleve Nov 12, 2026
ca09946
Update timeline annotations for passive recorded stays
kyleve Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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/AdaptivePaneLoadFixture.swift",
"Where/WhereUI/Tests/Support/TestStore.swift",
],
extraPackageProducts: ["SnapshotKitTesting"],
environmentVariables: snapshotEnvironment,
),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 10 additions & 3 deletions Where/WhereUI/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -160,9 +160,13 @@ 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.
- 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.
Expand Down Expand Up @@ -296,6 +300,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 `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.
`WhereStylesheetEnvironmentTests` covers the `@Environment(\.stylesheet)`
glue and `whereBroadwayRoot()` seeding, including the WhereWidgets path.
Expand Down
73 changes: 56 additions & 17 deletions Where/WhereUI/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -69,6 +69,61 @@ 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 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.
Timeline stays also measure eagerly so resizing cannot shift the content origin.
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 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. 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.
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.

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.
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.
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.
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
Expand Down Expand Up @@ -699,19 +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.

## 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.
10 changes: 10 additions & 0 deletions Where/WhereUI/SnapshotTests/DuoLayoutLabViewSnapshotTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import SnapshotKitTesting
import Testing
@testable import WhereUI

@MainActor
struct DuoLayoutLabViewSnapshotTests {
@Test func duoLayoutLab() async {
await assertSnapshots(of: DuoLayoutLabView.self)
}
}
20 changes: 19 additions & 1 deletion Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift
Original file line number Diff line number Diff line change
@@ -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 AdaptivePaneLoadFixture.State.allCases {
for pose in [SnapshotConfiguration.DuoPose.innerPortrait, .tabletop] {
let fixture = try await AdaptivePaneLoadFixture(state: state)
await assertSnapshots(
of: LocationsView(report: fixture.report)
.adaptivePaneReservedRegions(pose.regions)
.whereBroadwayRoot(),
named: "Adaptive-" + state.rawValue + "-" + pose.rawValue,
configurations: [SnapshotConfiguration(device: pose.frame)],
settle: .settledAtLeast(minDuration: 1),
)
await fixture.finish()
}
}
}
}
20 changes: 19 additions & 1 deletion Where/WhereUI/SnapshotTests/YearViewSnapshotTests.swift
Original file line number Diff line number Diff line change
@@ -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 AdaptivePaneLoadFixture.State.allCases {
for pose in [SnapshotConfiguration.DuoPose.innerPortrait, .tabletop] {
let fixture = try await AdaptivePaneLoadFixture(state: state)
await assertSnapshots(
of: YearView(report: fixture.report)
.adaptivePaneReservedRegions(pose.regions)
.whereBroadwayRoot(),
named: "Adaptive-" + state.rawValue + "-" + pose.rawValue,
configurations: [SnapshotConfiguration(device: pose.frame)],
settle: .settledAtLeast(minDuration: 1),
)
await fixture.finish()
}
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading