Clean successor to V3 (forge: lilith/atlilith). Seeded from local Mac working tree at ~/Code/@projects/@cocottetech/. node_modules and build artifacts excluded via .gitignore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
multi-leg-tour-planning.screen
Multi-leg tour route comparison — the traveling-salesman optimizer's user-facing decision surface. Implements brief R §R6 (R6a–R6j). Reached when Quinn says "plan me a tour across Berlin, Paris, Amsterdam in October" or via the touring drawer (R §R2a) → Calendar tab → Plan multi-leg tour. Voice register: working (decision surface) — strategist proposes, Quinn picks. Plain on jurisdiction / pre-lock conflict; hearth on the ambient optimizer-running cue.
Sibling to tour-leg-detail.screen.md (per-leg interior after approval) and hotel-scout.screen.md (per-city hotel scouting that runs in parallel after approval).
Layout (three-candidate route comparison card)
┌─────────────────────────────────────────────────┐
│ ◄ Touring ⋯ menu │ 56pt — top bar
├─────────────────────────────────────────────────┤
│ │
│ Multi-leg tour · October │ title + window
│ Three routes ready. All start & end SF. │ working register
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ ● #1 · recommended │ │ selected (rose accent)
│ │ Berlin Oct 3–8 → Paris Oct 9–12 → │ │
│ │ Amsterdam Oct 13–16 │ │
│ │ 14 nights · €5,200 hotels + €1,840 transit │ │
│ │ Projected €11,400 · net €4,360 │ │
│ │ ↑ €240 vs #2 │ │ RevenueDelta chip
│ │ Anchors: TG Mixer · Fashion-Week tail · │ │ AnchorList
│ │ Pride afterglow │ │
│ │ [ See the full plan ↗ ] │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ ○ #2 · easier pace │ │
│ │ Amsterdam Oct 3–7 → Berlin Oct 8–13 → │ │
│ │ Paris Oct 14–17 │ │
│ │ Net €4,120 · cheaper transit │ │
│ │ ─ €240 vs #1 · TG Mixer missed │ │ RevenueDelta + warn
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ ○ #3 · two cities │ │
│ │ Berlin Oct 3–10 → Paris Oct 11–17 │ │
│ │ (Amsterdam skipped) │ │
│ │ Net €3,890 · easiest pace │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ [ Re-run with constraints ] [ Pick #1 ] │
└─────────────────────────────────────────────────┘
Components
| Component | Notes |
|---|---|
| Top bar | Back to touring drawer; ⋯ menu: save-as-draft / cancel / view-inputs. |
| Title block | Window label ("October") + working-register subtitle. |
RouteCard (×3) |
Variant of the leg card; selected variant has rose accent + filled radio; unselected uses outlined radio. Each shows city sequence, dates, totals, anchor highlights, RevenueDelta chip. |
RevenueDelta chip |
Signed delta vs the top candidate (↑ €240 / ─ €240 / ↓ €470). Plain numerals — never "optimal." Hidden on #1. |
AnchorList |
Compact event anchors per leg (one line max per city). Tap a route → expanded breakdown opens with full AnchorList. |
WarningChip |
Two trigger sources: K §K4 jurisdiction conflict ("Vienna blocked — your rules") and R6-Q2 pre-lock conflict ("Adina locked — Berlin nights fixed"). Plain register; replaces the RevenueDelta chip slot when active. |
| Footer | Re-run with constraints opens the constraints sheet; Pick #N commits the selected route and triggers R6e fan-out. |
States
- Typical 3-route compare (default) — three RouteCards, top selected by default, RevenueDelta chips on #2 and #3.
- Loading optimizer (hearth — per R6i) — single ambient card: "Strategist is folding in the transit costs. Twenty seconds." Skeleton outlines for three cards beneath.
- Only 1 route fits constraints — one RouteCard at full width + soft banner: "Only one route fits your constraints. Loosen any to see alternates." Footer keeps
Re-run with constraints. - Re-run-with-constraints sheet open — bottom sheet covers ~60% of viewport; backdrop dims the RouteCards.
- Mid-tour reroute entry (from active-leg banner per R6f) — title changes to "Re-route from Berlin · 8 days left," #1 starts from current city; "cancel old legs" inline note explains audit threading per brief I §I4.
- Jurisdiction-conflict warning (per R6-Q3) — the route containing the blocked city is hard-filtered before display; a footnote chip under the surviving routes reads (plain): "Vienna dropped — your jurisdiction rules block it. See K §K4." Tap → settings.
- Pre-lock-conflict (per R6-Q2) — Quinn pre-locked a hotel in a city but the optimizer's preferred dates conflict. WarningChip on the affected RouteCard (plain): "Adina locked Oct 4–8 — Berlin nights pinned. Net recalculated." Tap → unlock affordance.
- Tie-break within 5% (per R6-Q1) — top two routes within 5% net both render with selectable radios + an explainer line: "These two are within 5% on net. The first earns more; the second is an easier pace. Your call." No
#1 recommendedlabel. - Optimizer returned no routes — see Edge cases.
- VoiceOver — read order: title → state explainer (if any) → RouteCard #1 (full) → RouteCard #2 (full) → RouteCard #3 → footer actions. WarningChips read as live regions.
- Reduced motion / Dynamic Type XXL — RouteCards stack vertically (already default); RevenueDelta numeric only; no animated pulse on warnings.
Re-run constraints sheet
┌─────────────────────────────────────────────────┐
│ Re-run with constraints ✕ │
├─────────────────────────────────────────────────┤
│ Add city [ + Lisbon ] │
│ Remove city [ ✕ Paris ] │
│ Cap nights total [ ≤ 10 ] │
│ Lock dates [ Amsterdam Oct 13–16 ] │
│ Cap budget [ ≤ €6,000 ] │
│ Preference [ prefer warmer cities ] │
│ │
│ [ Cancel ] [ Re-run optimizer ] │
└─────────────────────────────────────────────────┘
Per R6d: each constraint change re-runs the optimizer; new top-3 regenerate in <2s (search space ≤5 cities). Constraint chips persist as filter pills above the RouteCards on return; tap × to clear and re-run. Locked-hotel rows surface here too (read-only, with "unlock" affordance routing to the hotel-scout sibling).
In-the-wild copy (per R6i)
Discovery surface (working — ai-copilot opens the door):
Three cities in Europe cluster in October. Want strategist to draft a route across all three? Some of them, none of them, just the best one — your call.
Optimizer running (hearth — ambient cue while strategist composes):
Strategist is folding in the transit costs. Twenty seconds.
Comparison card landing (working — Quinn deciding):
Three routes ready. The first nets €4,360 across all three cities. The other two trade revenue for an easier pace. Tap to see the full plan.
Mid-tour re-route (plain — operational urgency):
Paris cancelled Oct 11. Re-route from Berlin with the remaining 8 days? Strategist has three alternates.
Tour-close synthesis (hearth — per brief Q §Q2, lands the morning after the final leg closes):
October closed at €4,820. Plan was Berlin–Paris–Amsterdam, ran Berlin–Amsterdam. Paris-cancelled wasn't your call. Amsterdam over-performed. Worth queueing again next October.
Gestures
- Tap a RouteCard → selects (radio fills, accent moves);
Pick #Nlabel updates to match. - Tap "See the full plan ↗" on selected card → per-leg breakdown sheet (each city expands into a pre-filled R §R1b planning sheet).
- Long-press a RouteCard → "Save as draft" / "Compare side-by-side" / "Why this route?" (strategist explanation).
- Tap a WarningChip → routes to K settings (jurisdiction) or hotel-scout sibling (pre-lock).
- Tap
Re-run with constraints→ sheet rises from bottom. - Tap
Pick #N→ confirms route; fires R6e fan-out (creates Ntour_legsrows under onetour_idparent in one transaction;bookings-hotelsscout fires per city in parallel; per-legtour-leg-detail.screenbecomes reachable). - Swipe-down on sheet → cancels constraint edits, restores prior routes.
- Pull-to-refresh on root → re-runs optimizer with current constraints (cheap).
- VoiceOver focus order matches the read order in States §10.
Edge cases
- Quinn cancels mid-optimization — back-tap or ⋯ menu → cancel: strategist receives abort signal; partial routes discarded; surface returns to touring drawer. No audit row (no decision was made).
- Optimizer can't find a valid route (all candidate sets violate hard constraints — jurisdiction + budget + dates collide) — single full-width card replaces the three RouteCards (plain): "No route fits these constraints. The hard blocks: {list}. Loosen one and re-run." Footer hides
Pick; onlyRe-run with constraintsremains. - Selected route's hotels all skip post-approval — after Quinn picks #1 and the fan-out fires, if every city's
bookings-hotelsscout returns dead-end (per hotel-scout.screen state 3), a follow-up approval card lands in chat (plain): "Berlin and Paris hotels: no acceptable matches. Re-scout with softer filters, swap cities, or cancel the tour?" Thetour_idparent remains inplanningstate until at least one leg has abookedhotel.
Related
- Brief R §R6 — parent design (R6a inputs · R6b objective · R6c card · R6d constraints sheet · R6e fan-out · R6f mid-tour · R6g audit · R6h specialist routing · R6i copy · R6j open Qs).
- specialist-strategist.contract.md — optimizer logic owner (per R6h; not
bookings-hotels, which is per-leg operational). - Brief K §K4 — jurisdiction hard-filter source for state 6.
- Brief T §T5 — tour ledger; multi-leg tours show as a single row threaded by
tour_id. - tour-leg-detail.screen.md — sibling; opens per-leg after Quinn picks a route.
- hotel-scout.screen.md — sibling; per-city scout fires in parallel after R6e fan-out and is also the unlock target for state 7 pre-lock conflicts.
- Brief I §I4 — counter-action threading for mid-tour reroute (state 5).
- Brief Q §Q2 — tour-close synthesis copy.