Skip to content

Restructure Where navigation into Locations / Your Year / Settings#119

Merged
kyleve merged 51 commits into
mainfrom
cursor/where-nav-restructure
Jul 23, 2026
Merged

Restructure Where navigation into Locations / Your Year / Settings#119
kyleve merged 51 commits into
mainfrom
cursor/where-nav-restructure

Conversation

@kyleve

@kyleve kyleve commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Summary

Restructures the Where app's top-level navigation, both in the UI and in how the structure is represented in code. The old four tabs (Primary, Elsewhere, Resolve, Settings) become three: Locations, Your Year, and Settings.

  • Locations — today's Primary content (headline region cards), with Elsewhere folded in at the bottom as a conditional entry card (shown only when there are secondary regions) that pushes the Elsewhere list, and Resolve folded in as a toolbar button that appears — badged with the count — only while there are data issues to fix.
  • Your Year — the calendar and timeline for the selected year, switched by a floating Liquid Glass pill at the bottom (Photos-style) that slides between the two lenses; the navigation title reflects the selected lens and the on-device activity summary sits in the leading toolbar.
  • Settings — gains a Data group at the top with rows for Attachments, Logged Days, and Regions (Regions moved here from the tracking group). These drill into the existing evidence / logged-days / regions screens.

Elsewhere and Resolve are no longer tabs, so the hideEmptyTabs preference (which only existed to hide them) is removed.

Calendar visual pass

Alongside the restructure, the calendar in Your Year got a polish pass:

  • Month cards get a subtle background and border; the current month is marked with a heavier accent border (an outer glow was tried and dropped as too much).
  • Future months are dimmed, and only the single next month is shown, as a short "teaser" peek that clips and fades out over one shared height (so the fade and the cutoff can't drift into a hard line).
  • Day cells render region-colored dots grouped into a subtle "stay pill" that runs from the first to the last day in a region, rounding subtly across week boundaries.
  • Scroll position is preserved when switching top-level tabs (the calendar positions to the current month once per year on first appearance via a hide-until-positioned reveal, then keeps the user's scroll), and tapping a month no longer pushes the timeline.

Notable changes

  • Extracted chrome-free content views (CalendarContentView, and EvidenceListView/LoggedDaysView without their own NavigationStack) so the calendar/timeline can be hosted by Your Year and the evidence/logged-days screens can be pushed from Settings; CalendarView remains a thin sheet wrapper for the region-focused calendar. SecondaryViewElsewhereView (folder SecondaryElsewhere); the timeline sheet shell is gone (PresenceTimelineViewPresenceTimelineList).
  • EvidenceListView and LoggedDaysView now conform to SettingsSection (searchable + routable) and are reached via new .attachments / .loggedDays settings destinations in a new top SettingsListSection.
  • ResolutionView is presented as a sheet from Locations and gains a Done button.
  • The Locations tab follows system appearance (the forced dark "passport" treatment was dropped).
  • New ElsewhereCardStyle stylesheet token for the Elsewhere entry card; the card shows a region count (not a summed day total, which would double-count multi-region days). New calendar month/day tokens (backgrounds, borders, current-month border, teaser peek height, day-dot sizing) pinned in WhereStylesheetTests.

Modeling / conventions

  • MainTabs TabID is now locations / year / settings; tab and row strings live in Strings.swift + the string catalog, with dead entries pruned.
  • Every SettingsDestination stays registered in the search catalog and in exactly one list section (guarded by SettingsSearchTests).
  • Follows the repo conventions: per-component stylesheet groups pinned in WhereStylesheetTests, one commit per step.

History / iteration

Earlier revisions of this branch introduced a fourth "Your Data" tab and a generic SegmentedTabContainer (persisted, animated segment switching), and Your Year briefly used a navigation-bar segmented control. After seeing them in the UI those were reverted: Your Year now uses the bottom floating pill, the data screens moved into Settings, and the container was deleted. The commit history shows the full progression.

Testing

  • ./swiftformat --lint clean.
  • The WhereUITests bundle (covering all changed views, strings, stylesheet, and settings search) passes on iPhone 17 / iOS 26.2, and the whole workspace builds.
  • Note: local full-scheme runs intermittently hit a shared StuffTestHost bootstrap crash (a simulator/test-host flake, not an assertion) and WhereResetTests.resetReturnsToOnboardingWithDataErased() is separately flaky (passes in isolation); both are unrelated to this change. CI's fresh runner is the authoritative full-suite check.

kyleve added 15 commits July 22, 2026 12:01
Drop the hideEmptyTabs preference, its YearReportModel mirror, the
Appearance settings toggle, associated strings/catalog entries, and the
covering test. Groundwork for the navigation restructure, which folds
Elsewhere into Locations and turns Resolve into a conditional toolbar
item so the setting has nothing left to gate. MainTabs keeps hiding the
now-unconditional Elsewhere/Resolve tabs by content only until it is
rewritten.
Split the calendar/attachments/logged-days screens from their sheet
chrome so the new segmented tabs can host them directly:

- CalendarView becomes a thin sheet wrapper (NavigationStack + Done +
  region/year title) around a new chrome-free CalendarContentView that
  keeps the month-grid load and the day->timeline push.
- EvidenceListView and LoggedDaysView drop their NavigationStack, Done,
  and title, keeping their + toolbar, filter, navigationDestination, and
  nested add/edit sheets so they merge into a host tab's bar.
- SecondaryView is renamed ElsewhereView (folder Secondary -> Elsewhere)
  and loses its NavigationStack; it is now pushed within the Locations
  stack. Previews wrap the chrome-free views in a NavigationStack.

Groundwork only; PrimaryView still presents these as sheets until the
Locations rework. PresenceTimelineView (the timeline sheet shell) is
kept for now and removed once PrimaryView stops using it.
A reusable segmented-control container the Your Year and Your Data tabs
will share: a segmented Picker over a String-backed SegmentedItem enum
whose selected segment persists across launches via @AppStorage (keyed
by a typed SegmentStoreKey), driving a content builder. Segment swaps
animate with a Reduce-Motion-aware directional slide, timed by a new
motion.segmentTransition stylesheet token.
YearView hosts the calendar and timeline for the selected year in one
SegmentedTabContainer (Calendar | Timeline), so the two views of the
same year live on one screen with the segment remembered across
launches. The on-device activity summary moves here as a toolbar item
(sparkles) presenting RecentActivitySummaryView. Adds the tab/segment
strings it needs. Not yet wired into MainTabs.
YourDataView hosts the evidence archive and the hand-logged-days list in
one SegmentedTabContainer (Attachments | Logged Days), remembered across
launches. Each segment's content view brings its own + toolbar item, so
the add action follows the active segment. Not yet wired into MainTabs.
Rename PrimaryView to LocationsView and reshape it into the new hub:

- Drop the recent-activity/timeline/calendar/evidence/logged-days
  toolbar sheets (they now live in Your Year / Your Data), keeping the
  region-card focused-calendar sheet.
- Fold Elsewhere in at the bottom as a conditional ElsewhereSummaryCard
  (a new glass entry card, its own ElsewhereCardStyle stylesheet token)
  that pushes ElsewhereView — shown only when there are secondary
  regions.
- Add a Resolve toolbar button that appears only while
  dataIssueCount > 0, badged with the count, presenting ResolutionView
  as a sheet (which gains a Done button).

Delete the now-unused PresenceTimelineView sheet shell (file renamed to
PresenceTimelineList.swift). MainTabs still lists the old Elsewhere and
Resolve tabs until its rewrite; tests/previews updated to the new names.
Replace the conditional Primary/Elsewhere/Resolve/Settings bar with a
fixed Locations / Your Year / Your Data / Settings bar. Elsewhere and
Resolve are no longer tabs (folded into Locations), so all the
content-conditional visibility, sticky-selection, and tab-badge logic is
gone. New TabID cases + labels + SF Symbols.
- Prune the now-dead tab.primary / tab.elsewhere / timeline.done
  strings and catalog entries; add tab.locations / tab.year / tab.data,
  the segment picker labels, the Attachments segment label, and the
  Elsewhere card subtitle to the catalog.
- Add hosting tests for YearView, YourDataView, CalendarContentView, and
  ElsewhereSummaryCard; retarget the renamed Primary/Secondary/timeline
  tests; add SegmentedTabContainerTests covering persisted-selection
  restore across launches.
- Update the WhereSession doc comment: the data-issue count now drives
  the Locations Resolve toolbar badge rather than a Resolve tab.
- Remove the Locations tab's forced dark colorScheme, dark toolbar
  scheme, and elevated dark gradient (they also leaked into the pushed
  ElsewhereView). The tab now follows system appearance.
- In the elsewhere-only state (tracked days but none in a headline
  region), surface a 'See Elsewhere' link to ElsewhereView and drop the
  copy that pointed at the now-removed Elsewhere tab.
When the selected year isn't the current one, the Your Year and Your
Data tab titles gain a grouping-free year suffix (e.g. "Your Year ·
2024"), so which year is on screen is never ambiguous; the current year
keeps the plain title.
Keep every segment mounted (only the selected one visible, hit-testable,
and exposed to VoiceOver) and slide between them via a container-width
offset, instead of re-identing the content on each switch. Switching now
preserves each segment's state — Logged Days filter, calendar/list
scroll, loaded data — rather than tearing it down and reloading.

The content builder now also reports whether a segment is selected;
EvidenceListView and LoggedDaysView use it to contribute their + toolbar
item only while active, so the hidden-but-mounted segment doesn't add a
duplicate. Segmented controls also get a per-tab accessibility id.
- ElsewhereSummaryCard shows only the region count (not a summed day
  total, which double-counted multi-region days).
- Refresh doc comments that still referenced the removed Primary/Elsewhere
  tabs (CardStyle, RegionSummaryCard, YearTotalsWidgetView).
- Drop the now-unused report property from AppearanceSettingsView and its
  call sites/preview.
- (SegmentedTabContainer already gained per-tab accessibility ids.)
Replace the in-content segmented container with a segmented control in
the navigation bar (Liquid Glass) that switches between the calendar and
timeline, crossfading between them, alongside the activity-summary
button. Drops the year-suffixed title (the control owns the bar) and the
YearSegment type.
… Your Data tab

Remove the Your Data tab and instead surface its screens from Settings:
a new top group holds Attachments, Logged Days, and Regions (Regions
moved here from the tracking group). Adds .attachments/.loggedDays
settings destinations (EvidenceListView/LoggedDaysView now conform to
SettingsSection so they're searchable and routable); both regain their
navigation title and lose the segment-only isActive gating. MainTabs is
now three tabs (Locations, Your Year, Settings).
With Your Year using a toolbar toggle and Your Data gone, nothing uses
SegmentedTabContainer — remove it (and SegmentStoreKey, SegmentedItem,
its test, and the motion.segmentTransition token). Prune the dead tab/
segment strings (tab.data, the year/data forYear titles, the segment
picker labels) and add catalog entries for the new Settings Data rows;
refresh the SettingsView doc comment.
@kyleve kyleve changed the title Restructure Where navigation into Locations / Your Year / Your Data / Settings Restructure Where navigation into Locations / Your Year / Settings Jul 22, 2026
kyleve added 14 commits July 22, 2026 15:24
Place the + on the right (topBarTrailing) on both the Attachments and
Logged Days screens.
Move the activity-summary button and the calendar/timeline segmented
control to the leading side of the bar, switch the control to icons
(calendar / timeline glyphs), and title the page after the selected
view (Calendar or Timeline).
Keep the activity-summary button on the left; put the calendar/timeline
control on the right.
The calendar auto-scrolls under the bar while the timeline starts at the
top, so switching animated the bar's scroll-edge material in/out (a
toolbar fade). Pin the nav bar background visible so it's consistent
across both views.
The SettingsDestination doc said 'eight' groups; there are now nine
(attachments + logged days added). Also remove the empty Sources/Data
directory left after deleting YourDataView.
Switch tabBarMinimizeBehavior from .onScrollDown to .never.
The ScrollView's onAppear re-fired scrollToCurrentMonth on every
reappearance — including returning to the Your Year tab — snapping the
grid back to today and losing the user's scroll position. Guard it on
the year already scrolled so it jumps once per year; a report-year
switch rebuilds the content and scrolls the new year afresh.
Replace the top-bar segmented control with a Photos-style Liquid Glass
pill floating at the bottom-center (above the tab bar), reframing the
switch as zooming between the year overview (timeline) and month detail
(calendar). The selected segment slides between options; the page title
still tracks the mode and the activity-summary button stays in the bar.
Replace the small region dots under each day number with a full-width
region-colored bar (split into equal segments on multi-region days), so
contiguous stays read as horizontal color bands across a week. Adds
regionBarHeight / regionBarCornerRadius CalendarStyle tokens (pinned in
WhereStylesheetTests); the month footer still uses dots for its tally.
Replace the full-width region bar with region-presence dots (restored,
bumped to 8pt) plus a subtle region-tinted pill drawn behind contiguous
same-region days. The pill's true ends round fully; where a run spills
across a week boundary the edge rounds subtly (continuationRadius) to
imply it carries on, and same-row neighbours bleed half the grid gap so
a stay reads as one connected shape. Swaps the regionBar* tokens for a
RegionBand group and a dayDotSize token (pinned in WhereStylesheetTests).
Bump the stay pill's terminal-end radius (10 -> 14) so run ends read as
fuller rounded caps. On multi-region days, overlap the region dots by
2pt into a cluster, each with a background-colored rim so they read as
distinct coins. Adds dayDotOverlap / dayDotStrokeWidth tokens (pinned in
WhereStylesheetTests).
Reduce the day-number-to-dots gap to 0 (dots sit snug beneath the date)
and give the stay pill vertical breathing room by hugging the padded
content, so the pill no longer butts against the bottom of the dots.
Adds dayNumberDotSpacing; retunes regionBand.verticalInset (3 -> 4,
now content padding). Pinned in WhereStylesheetTests.
Apply the background-colored stroke to all region dots, not just the
overlapping multi-region cluster, for a consistent look.
…re bottom gap

- Use a single selection capsule that follows the selected segment via
  matchedGeometryEffect(isSource:) so it slides instead of fading.
- Fill the selection with Color.primary (black in light, white in dark),
  with system-background text for contrast.
- Enlarge the segment icons (.imageScale(.large)).
- Increase the pill's bottom padding so it clears the tab bar.
kyleve added 18 commits July 22, 2026 17:14
The .glassEffect wrapped the selection capsule, frosting the black to
grey. Split the glass into its own background layer behind the selection
capsule so Color.primary reads as real black (white in dark mode).
Bump the pill's inner padding (xxSmall -> small) so the selection
capsule has more breathing room from the glass control's edges.
Tapping a day no longer pushes the timeline (the timeline is now a
first-class Your Year segment). Drops the day cell buttons and the
timelineTarget navigation destination, the now-unused scrollToMonth
plumbing in PresenceTimelineList, and the dead timeline.title string.
Pin each segment label to its intrinsic width (.fixedSize) so it can't
truncate while the segment frame animates, and use a non-overshooting
.smooth spring so the sliding capsule no longer squishes.
Add a footerDividerSpacing token (8pt) so the region tally rows don't
butt up against the divider under each month. Pinned in
WhereStylesheetTests.
Bring back .snappy (smooth animated only one direction). The label's
.fixedSize already prevents the truncation that prompted the swap, so
snappy is safe to keep.
Float the pill over the scrolling content (content now scrolls under it,
with a measured bottom content inset so the last rows clear it) and add
a bottom blur band — an ultraThinMaterial masked by a vertical gradient
(clear a bit above the pill, full at the bottom) — so the content
dissolves into blur toward the screen bottom. Fade height derives from
the measured pill height and bottom safe area.
Make the fade band taller (start the blur earlier above the pill) and
move the gradient's full-blur stop up to 0.5 so it's solid well before
the bottom.
Revert to the plain floating pill in a safeAreaInset (content stops above
it); remove the bottom fade, the scroll-under contentMargins, and the
geometry measurements it needed.
Replace the ScrollViewReader + onAppear + async scrollTo (which scrolled
to the current month a frame or two after the grid appeared, causing a
visible jump when switching to the tab) with .scrollPosition(id:). The
target is seeded before the months are published, so the grid opens
already scrolled to the current month; it then just tracks scrolling.
Still guarded to seed once per year, preserving position across tab
switches.
.scrollPosition couldn't jump to an off-screen lazy month on first
layout (it stayed on January). Go back to ScrollViewReader.scrollTo but
keep the grid hidden (opacity 0) until it's scrolled into place, then
reveal — so the calendar appears already at the current month instead of
visibly jumping. Still guarded once per year to preserve scroll across
tab switches.
…d future

Give every month a subtle grey card background (not just the current
one), make the current-month highlight a bit bluer (accent 0.08 -> 0.14),
and dim months entirely in the future (opacity 0.55) so it's clear they
haven't happened. Future is derived in the view from the report's
reference month. Adds month.background / month.futureOpacity tokens
(pinned in WhereStylesheetTests).
Instead of rendering all future months, drop everything after the next
month and render that next month as a 'teaser': clipped to a short peek
height (so the grid can only scroll partway into it), dimmed, and faded
out over that height with a gradient mask. Past years (no current month)
still show every month. Replaces the blanket future-month dim with a
teaserPeekHeight token.
Lighten every month's background (0.05 -> 0.03) and give each card a 2pt
darker border. The current month gets a 4pt accent border plus a matching
accent outer glow (shadow). Adds border/borderWidth/currentBorder/
currentBorderWidth/currentGlow/currentGlowRadius tokens (pinned in
WhereStylesheetTests).
Use a gentler multi-stop gradient (70% at mid, 25% at 0.85, clear at the
bottom) so the peek eases out gradually instead of zeroing out so fast.
Drop the month corner radius 22 -> 21, bump the current month's glow
opacity (0.4 -> 0.55), and lighten the current month's fill (0.14 -> 0.1)
and border (accent -> accent 0.7).
Reduce currentMonthHighlight back to accent 0.08 (its original value) —
the accent border and glow now carry the emphasis, so the bluer fill was
too much.
Remove the accent-tinted outer glow on the current month (and its now-unused
currentGlow/currentGlowRadius stylesheet tokens). Extract the next-month teaser
clip + fade into a single TeaserPeek modifier keyed off one height, so the
gradient reaches fully transparent exactly where the content is clipped — no
hard cutoff line.
/// Spacing inside a day cell (the number over its dots).
/// Diameter of a region-presence dot under a day number (a touch larger
/// than the footer dots so days scan easily).
var dayDotSize: CGFloat

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of "day" items here. These should be grouped into one nested type. Don't we cover this in AGENTS.md?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Done in b62fbce — folded the flat day* scalars, the today/unresolved markers, and the evidence badge into a nested DayStyle (calendar.day.*), per the AGENTS.md "group large flat types into sub-structs" rule. Footer dotSize and regionBand stay on CalendarStyle. Values unchanged.

/// Subtle wash behind every month, so each reads as its own card.
var background: Color
/// Border around every month card (a touch darker than `background`).
var border: Color

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have past/current/future here in terms of past months / current month / future months. Let's group the styling for a month into a single type and then reuse it.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Done in c530e2f — replaced the parallel background/border/borderWidth (all months) and currentMonthHighlight/currentBorder/currentBorderWidth (current) with a single nested MonthStyle.Card { fill, border, borderWidth }, exposed as plain (past/future) and current. MonthGridView now picks one Card by isCurrentMonth and reads both from it instead of ternary-ing each property. Values unchanged.

var futureOpacity: Double
/// Height the next month is clipped to — a peek the grid can only
/// scroll partway into, faded out over it.
var teaserPeekHeight: CGFloat

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

futurePeekHeight? But also, can't we only determine this at view time? How would we figure this out in the stylesheet given it relies on layout?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Renamed to futurePeekHeight in d2fc2c5.

On the substantive point: you're right that a fixed 150 is an arbitrary clip, not a layout-derived value. I'm taking that on next as a follow-up commit — computing the peek at view time as a fraction of the measured next-month height (via onGeometryChange), so it actually shows a proportion of the month rather than a magic constant. Will follow up here when it lands.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Landed the view-time version in 9c6dd8f. futurePeekHeight is gone; the stylesheet now carries a dimensionless futurePeekFraction (0.5). TeaserPeek measures the next month's natural rendered height (fixedSize + onGeometryChange, decoupled from the clamped frame so the measurement can't feed back) and clips to naturalHeight * fraction, staying hidden until measured so the full month never flashes. So the stylesheet keeps only the style choice and the layout-dependent pixel height is derived where the layout is actually known.

kyleve added 4 commits July 22, 2026 19:31
Fold the flat day* scalars, today/unresolved markers, and the evidence
badge on CalendarStyle into a nested DayStyle (calendar.day.*), per the
AGENTS.md 'group large flat types into sub-structs' rule. Pure regrouping —
values unchanged; footer dotSize and regionBand stay on CalendarStyle.
Replace MonthStyle's parallel background/border/borderWidth (all months) and
currentMonthHighlight/currentBorder/currentBorderWidth (current) with a single
nested Card { fill, border, borderWidth } exposed as plain (past/future) and
current. MonthGridView now picks one Card by isCurrentMonth and reads both from
it instead of ternary-ing each property. Pure regrouping — values unchanged.
Match the past/current/future month vocabulary. Name-only change.
Replace the fixed futurePeekHeight (150pt) with a dimensionless
futurePeekFraction (0.5). TeaserPeek measures the next month's natural
rendered height (fixedSize + onGeometryChange, decoupled from the clamped
frame so it can't loop) and clips to naturalHeight * fraction, hidden until
measured so the full month never flashes. The stylesheet keeps only the
style choice (the fraction); the layout-dependent pixel height is derived
where the layout is known.
@kyleve
kyleve enabled auto-merge (squash) July 23, 2026 02:41
@kyleve
kyleve merged commit d5a0f48 into main Jul 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant