One button, three maps, one function
Look at a card in the recommended-places list at the top of this track: the place's name, my note, the trip it belongs to, an orange Go here on foot button with a walking figure, and a ⋯ menu that holds Show on map and Remove pin. The orange button is the whole point of the list. A recommendation Dad cannot act on in one tap is just a note.
The map offers the same act. On Apple's map a pin is a button. On Google's map, tapping the pink marker lands in mapView(_:didTap:). On Kakao's map, tapping the point lands in poiDidTapped. All four entry points, list and three maps, call one function: AppModel.walk(to:). That function refuses during a simulation or for an invalid pin, and otherwise wraps the pin as a PendingDestination whose source is .pin, then hands it to offer. There is no pin-specific routing anywhere. A pin becomes a Place (provider pippa, carrying its door when I named one) and walks the same road as every other destination from outside.
A pin is always a walk
Recommendations are things you walk to: a bar, a museum door, a bakery around the corner. So when a pin's destination is taken, the first line of take switches the travel mode: if case .pin = destination.source { setTravelMode(.walking) }. The route then comes from whichever provider is active, Apple, Kakao or Google, so the same tap works at home and abroad. The feature landed in ba5d7c5, the commit that answered todo #350.
Compare my map guides from the first lesson. Tapping a guide's place pin calls go(toGuide:label:), which only asks for a route preview in the current travel mode. It never starts guidance by itself. A recommendation is a stronger signal (I saved it on purpose, and Dad tapped Go), so it goes further, but it still stops at the same door when something else is running.
Never silently replace a route
Pathfinder's handoff rule is older than pins: a destination from outside (my navigate handoff, a pathfinder://navigate link, a pin I put on the map) never silently replaces an active route, or a route Dad is still choosing. offer is where that rule lives.
- Idle (no trip, and the stage is browse): a pin is taken at once. A route is asked, and guidance starts.
- Guiding or previewing: the pin waits in
pendingand a notice says Pippa sent and the place's name. A preview counts as busy, because Dad is in the middle of choosing. - Dad accepts:
acceptPendingends the running drive (its reason: "switched to a new destination") or cancels the preview first, and only then takes the pin.
take is careful at the end too. Guidance starts only if the routing it just asked for produced a fresh preview, from the same provider, to this very destination. A failed or cancelled route can never start an older preview that happened to still be on screen.
The app's test suite pins the behaviour down. testPinTapStartsWalkingAndWaitsBeforeReplacingADrive starts a driving trip, taps a pin (the drive keeps going, the pin is pending), accepts (now it is a walk to the pin), ends it, taps another pin while idle (a walk starts at once), and finally checks that the provider was asked for exactly [.driving, .walking, .walking].
The button that lost its words (#383)
On a walk in Seville, Dad opened the list and every card showed an orange capsule with no text. Todo #383 is that regression, not the feature's birth. It came right after todo #363 moved the button onto CwkKit's shared .prominentControl style, so that labels would stay on one line.
The cause was two SwiftUI behaviours stacking. The kit's control label fixed the button's size, which asks the label for its ideal size. Inside a List row, a Label answers that proposal with its icon alone. Then the row tinted that icon with the accent colour, which was the capsule's own fill. Orange on orange, no title. Outside a List the same button looked fine, which is why the kit's own suite never saw it.
The fix landed in two places. cwkSiblingKit b65cbab made OneLineLayout offer the label a wide proposal instead of asking for its ideal size, so every family app on that style got it at its next build. Pathfinder's button now names .labelStyle(.titleAndIcon), so the icon is white on the fill. The kit deliberately leaves label styles to callers, so a caller's .iconOnly still wins. And a new test, ControlButtonVisibilityTests, draws the real sheet off-screen in dark and light, finds the first filled capsule and reads its pixels: the label must stand out from the fill and span at least 90% of its title's width. On the old code it finds zero label pixels.