Build 2 — Watch redesign + Share + in-the-wild fixes

The watch becomes a five-state, layout-budgeted runner that can no longer end the session; a new share sheet exports 9:16 / 1:1 image cards or markdown with optional Strava auto-attach; eight fixes from real-world use.

← All changes

Watch redesign

  • Five distinct layout states — heartRate, durationCountdown, restCountdown, idle, joining — each composed against an explicit per-size profile (41 / 45 / 49mm) keyed off screen height. Fixes the text-overlap bug on the smaller cases.
  • The Stop button only renders during a duration countdown, and now sends .stopDurationTimer (protocol v4 → v5) instead of ending the workout. The wrist can no longer end the session at all — that authority lives on the phone, restoring invariant #3.
  • A joining state appears whenever the phone says “running” but no state has arrived yet, with a 3-second fall-through to “iPhone session not found”.

The watch’s job hasn’t changed — HR + elapsed + a single allowed action — but the layout had grown crowded enough on the 41mm case to overlap. Rebuilding it as discrete states with size profiles means each surface gets exactly the room it needs.

Share

  • Dark-theme image cards rendered at 1080×1920 (9:16) and 1080×1080 (1:1), with strength (hero volume) and rehab (pain readout, never hero) variants. Avg HR omits cleanly when nil — no ”—” placeholders.
  • A modal with format chips (image 9:16, image 1:1, markdown) and a coral Share action. Entry from SessionSummaryView and from the History tab, so past sessions are covered too.
  • Markdown export shares a single rollup with the image card so the two always agree on volume, time-under-tension, and HR.
  • New attachPhotoToStrava setting (off by default, gated on uploadSource == .direct) auto-uploads the 9:16 card as a photo on the Strava activity right after the activity is created. Failures are non-fatal.

Sharing a session image is the first surface that lets you hand a friend or a Strava feed something you can read at a glance, without a log-in or an export. Off by default, like everything social.

Fixes

  • Strava timezone. The ISO8601 formatter now uses TimeZone.current for start_date_local — activities were appearing two hours off.
  • Watch auto-launch. startWatchApp now awaits HealthKit authorization first; previously it silently failed on the very first run, before auth had been granted.
  • Onboarding. Browse Library opens via .fullScreenCover instead of .sheet — the nested sheet was snapping closed when the parent recomputed. Quiz options and path tiles get .contentShape(Rectangle()) so taps on the padding count.
  • Keep screen awake. New setting (default on) gates isIdleTimerDisabled across the runner’s lifecycle, so the phone stops dimming mid-set on the tripod.
  • Duration sets. Actual elapsed seconds are now persisted on every user-driven stop (Stop button, watch stop, manual log). Reaching the planned duration without overtime auto-completes the set and starts rest.
  • Edit set values post-session. A new sheet on the summary lets you fix per-set reps, weight, or duration with the same pickers as the runner — for the inevitable “I logged 8 but did 9”.
  • No duplicate previous-set row. The runner’s spec strip lost its redundant ghost cell; the actionable “Repeat previous ↖” strip below already shows the same data.
  • Rest timer’s “Up next ↓” now crosses the exercise boundary — when the current exercise is done, the line previews the next exercise’s first set instead of going blank.