chore(planning): phase 5/6/7 planning artifacts + milestone-completion state sync

This commit is contained in:
araharah-executor
2026-10-25 12:51:27 +08:00
parent bb587ab537
commit db27aa8440
34 changed files with 3949 additions and 214 deletions
@@ -0,0 +1,440 @@
---
phase: 6
slug: turn-by-turn-guidance-live-driving
status: draft
shadcn_initialized: false
preset: none
created: 2026-10-24
---
# Phase 6 — UI Design Contract (Turn-by-Turn Guidance, Live Driving)
> Visual and interaction contract for the guidance experience. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
>
> **Source posture:** the design system is IN USE — `theme/dist/rams.css` tokens and the live shell (strip, card stack, recenter control, gantry, destination card, route card + chips) shipped in Phases 3–5 and passed their UI-SPECs + device gates. This contract specifies **extension** of that shell into the guidance cockpit: the top maneuver banner, the compacted location register, the collapsible maneuver list, the guidance card (the route card evolving), the abandon-route confirm card, trip-end messaging, the locked-follow camera, and the chat turn-card (a Telegram message, not app chrome). It invents no new token values, no new colors, no roundedness, no shadows. Binding upstream: 06-CONTEXT.md D-01..D-16 (layout, ETA deadband, haptics, PX-03/PX-01, camera, glyphs, honest copy, trip-end deletion), 05-UI-SPEC.md (the route shell this extends — ITS CONVENTIONS BIND), 03-UI-SPEC.md (strip/card/control/copy/a11y law), telemetry_rams/DESIGN.md §Navigation Heads-Up Display (the banner's component spec), DESIGN.md palette semantics (orange = navigation/primary, red = hazards/errors only). The `active_route_guidance` mockup is layout reference only; its lane matrix, incident band, speedometer deck, report/audio buttons, backdrop-blur header, and rounded puck are scrubbed residue (deferred features or existing-law violations — recorded in Fidelity Artifact).
>
> **Map-law restated:** the MapLibre style is never re-set; additions only (`addLayer`/`addSource`/`setData`). The route line stack from Phase 5 is guidance's route rendering — unchanged paint (orange 5px on black 9px casing). No route-line progress split this phase: the puck, the progress bar, and the camera carry progress (recorded call — less, but better).
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none — shadcn gate N/A (TypeScript + Vite, no UI framework; unchanged from Phase 3) |
| Preset | not applicable |
| Component library | `@araharah/theme` — `theme/dist/rams.css` (already imported via `shell.css`; nothing new to wire) |
| Icon library | inline SVG — 2px uniform stroke, square linecaps, orthogonal/45° baselines (app chrome); PLUS the phase's one new committed asset: the **8-arrow maneuver glyph set** — hand-drawn 32×32 SVGs at 3px stroke (the HUD spec's weight), rendered as inline SVG in-app and rasterized once to 256×256 PNGs (maintainer `build-glyphs.mjs`, sharp) for chat turn-cards. One geometry, two renderings; the committed SVGs are the single source |
| Font | self-hosted Geist + Space Grotesk via `theme.css` `@font-face` (unchanged) |
| Basemap | `theme/basemap/liberty-rams.json` — Rams-constant under both Telegram themes (unchanged) |
**Consumed component classes** (all pre-existing; none restyled locally): `.card`, `.card-header`, `.card-ref`, `.btn-primary`, `.btn-secondary`, `.text-headline-md`, `.text-body-md`, `.text-body-sm`, `.text-label-sm`, `.text-telemetry-num` — plus the Phase 3–5 shell classes (`.strip`, `.recenter`, `.route-stack`, `.card-stack`).
New CSS for this phase (banner, list, guidance-card rows, progress bar) lives in `shell.css` and references `var(--color-*)` / `var(--spacing-*)` / `var(--type-*)` only. The machined-geometry literal list grows by exactly: banner arrow plate 48×48 + glyph 32×32, banner min-height 96px, compact strip min-height 24px, list row min-height 48px + row glyph 24×24, next-row lamp 8px, list panel max-height 480px, progress bar 4px, the 64px control-clearance composite (reused), guidance camera paddings {top 320, bottom 64, left/right 64} (map-space px) — all 4-multiples, 1px/2px rules, machined glyph geometry, and map-space paint arithmetic — the established exception classes.
---
## Shell Layout — Register Bands (guidance occupation, D-06)
The Phase 3 partition is unchanged. During guidance the registers re-populate:
| Band | Content during guidance | Geometry |
|------|------------------------|----------|
| Top register | **Maneuver banner** (new, D-06) — distance + arrow + street, matte-black chassis; the toggle for the maneuver list | Below `safe-top + space-sm`; min-height 96px; full-bleed |
| Top register, compacted | **Location strip (compacted)** — same lamp + state word, telemetry cluster hidden, min-height 24px | Directly beneath the banner, separated by a 1px `chassis-subtle` rule; one continuous black chassis with the banner |
| Center register | Rams basemap + position marker + Phase 5 route stack; **maneuver list panel** (only while expanded, overlays the map — no dim layer, boot-card precedent); boot/denied/pending cards unchanged when guidance is not active | Map stays the dominant surface |
| Bottom register | **Guidance card** (the route card evolved — D-06); **abandon confirm card** (bottom placement — thumb-reach call, D-09); signal-lost card (existing, transient); recenter control bottom-right (returns on pan-unlock, Phase 3 semantics); attribution chip bottom-left | Card bottom offset `calc(var(--safe-bottom) + 64px)` — the Phase 5 composite, unchanged |
- **Strip compaction is layout-only.** The strip's state machine, lamp colors, and word labels are byte-identical (Phase 3 table stands; `strip.ts` logic untouched). Guidance adds a `data-guidance` attribute on the shell root: padding-block drops `space-sm` → `space-xs`, min-height 32 → 24px, and the right telemetry cluster (`±M` / `FIX Ns AGO`) hides — accuracy/fix-age are not glance data at speed. The strip never hides degradation silently: staleness and denial still announce via red lamp + word, unchanged. Compacted registration is honest registration.
- **No new floating controls.** The recenter control keeps its plate, notch states, and hit target exactly; during guidance its `following` state means *locked-follow* (D-10) — the notch semantics read identically to the user.
- Banner placement is the strip's own band + the space above it: the chassis grows upward, the map viewport shrinks correspondingly (camera arithmetic below compensates).
---
## Spacing Scale
Bound to tokens (`--spacing-*`), consumption not invention. Phase 3/5 tables stand; Phase 6 adds:
| Element | Number | Token source |
|---------|--------|--------------|
| Banner padding | `space-sm space-md` (8/16) | tokens |
| Banner min-height | 96px (register 12 + distance 32 + street 26 + 2×`space-xs` gaps + 2×`space-sm` padding ≈ 94 → 96 = 12×8) | tokens + spec geometry |
| Banner arrow plate | 48×48px (32px glyph + 8px breathing per side), `surface-dark` field on the black chassis (mockup nesting precedent, token `--color-surface-dark`) | machined geometry |
| Banner arrow glyph | 32×32 viewBox, 3px stroke, square linecaps | telemetry_rams HUD spec |
| Compact strip min-height | 24px, padding-block `space-xs` | tokens + spec geometry |
| Banner→strip separator | 1px solid `chassis-subtle` | machined rule |
| List panel | card geometry: `calc(100% - 2×margin)`, max-width 320px, centered; max-height 480px (scrolls); padding `space-sm` bottom | tokens + spec geometry |
| List row | min-height 48px, padding `space-sm space-md`, glyph 24×24, 1px `matte-black` row separators (dark: `inverse-on-surface`) | tokens + machined rule |
| Next-row lamp | 8×8px `signal-orange` square at the row's left edge (checkbox geometry precedent) | spec geometry |
| Guidance card | bottom offset 64px composite (Phase 5, reused); telemetry row + progress bar margins `space-xs`; progress bar height 4px, width = content width (inset `space-md`) | tokens + machined geometry |
| Confirm card | bottom register, same 64px composite (thumb-reach — see A11y) | Phase 5 composite |
| Guidance camera padding | `{ top: 320, bottom: 64, left: 64, right: 64 }` — map-space px (MapLibre padding, not CSS; 8-multiples; `T − B = 256 ≈ 0.30 × 844` reference viewport → center at 65% down-screen, D-10) | spec geometry |
Exceptions: none beyond the established classes (44px hit targets via existing slop patterns; 4-multiples; map-space arithmetic).
---
## Typography
The Phase 3 scale stands — zero new sizes, zero new weights. `telemetry-num` gains its second consumer (the banner distance).
| Role | Token | Spec | Usage in Phase 6 |
|------|-------|------|------------------|
| Banner distance | `--type-telemetry-num` | 28px Space Grotesk 700 / 32px, tracking −0.03em | Distance-to-maneuver number (`250` + `M`) — the glance datum |
| Banner street | `--type-headline-md` | 20px Space Grotesk 600 / 26px, tracking −0.02em | Street/designation line, uppercase (HUD spec: "Maneuver Typography: headline-md") |
| Banner register | `--type-label-sm` | 10px Geist 700 / 12px, tracking +0.08em, uppercase | `GUIDE // NEXT` register line; `LIST`/`CLOSE` toggle word |
| List row street | `--type-body-md` | 14px Geist 400 / 20px | Row street names |
| List row distance | `--type-label-sm` | 10px Geist 700 / 12px, uppercase | Per-row remaining distance (`1.2 KM`) |
| Guidance-card time | `--type-telemetry-num` | (as above) | Minutes remaining — the route card's time slot, evolved |
| Guidance-card units/ETA | `--type-label-sm` | (as above) | `MIN`, `{km} KM`, `ETA 18:42` |
| Card state line | `--type-body-sm` | 12px Geist 400 / 16px | Degradation copy (no-reroute), confirm-card body |
| Confirm headline | `--type-headline-md` | (as above) | `Abandon route?` |
| CTA labels | `--type-label-lg` (inside `.btn-*`) | 14px Space Grotesk 700 / 18px, uppercase | `END TRIP`, `YES, ABANDON`, `CONTINUE` |
Unit formats are instrument constants, code-composed (Phase 3/5 precedent): distance `{n} M` under 1000 (integer), `{n.n} KM` at/over (one decimal), ETA clock 24-hour `HH:MM`, minutes `{n} MIN`. Units unchanged under `ms`. Body line-height ~1.43, heading 1.3 — token ratios verbatim.
---
## Color
60 / 30 / 10 split unchanged. **The banner's arrow joins signal-orange's reserved-for list — navigation semantics, the Phase 1 reservation's natural fulfillment.** The reserved-for list is restated and extended:
| Role | Value | Usage in Phase 6 |
|------|-------|------------------|
| Dominant (60%) | Rams basemap grayscale + shell chrome (unchanged) | The world stays grayscale; guidance ink reads against it |
| Secondary (30%) | Instrument surfaces (unchanged) | Guidance card + list panel + confirm/trip-end cards: light `soft-beige` / dark `inverse-surface` via theme slots; banner + compact strip chassis `matte-black` (Rams-constant, strip precedent) |
| Accent (10%) | `signal-orange` `#EB8008` | **Reserved-for list (exact, Phase 6 — supersedes the Phase 5 list where guidance surfaces are concerned):** every Phase 5 reservation (route line, heading cone, follow notch, focus brackets, lamps, primary CTA fill, press inverts); PLUS the banner maneuver arrow (32×32, 3px stroke — HUD spec); the machine-blink gantry square in REROUTING/RESUMING states (boot-card precedent); the next-maneuver row lamp + row glyph in the list; the progress-bar fill. Orange appears at most once per band, always meaning navigation |
| Destructive | `functional-red` `#ED3F1C` | **Unchanged and NOT extended:** denied/signal-lost lamps only. Abandoning a route is a deliberate user action, not an error — the confirm card renders in standard card ink; the REROUTING and no-reroute states are deferrals, never failures — no red anywhere in guidance chrome |
| Operational | `olive-green` `#736B1E` | LIVE lamp only (unchanged) |
**Banner ink hierarchy (chassis discipline — three inks + one accent):** register line `chassis-subtle` (≈9:1 on black), street `soft-beige` (≈11.6:1 — the HUD spec's ink), distance `pure-white` (≈17:1 — maximum glance contrast), arrow + state square `signal-orange`. The HUD spec pins the arrow orange and the street soft-beige; the distance reads white (mockup precedent: `250 M` in pure-white) so the accent stays singular per band.
**Turn-card PNG colors (D-12):** stroke `signal-orange` `#EB8008` on a `matte-black` `#1A1A1A` full-bleed plate — the banner's chat sibling; hex values copied verbatim from `tokens.css` as build constants (DSGN-01 precedent; the glyph script's source SVGs carry the literals).
- **Opacity 1.0 everywhere** — physical planarity law. The one sanctioned translucency remains the Phase 3 accuracy circle (data grounding).
- **Past-maneuver rows** render in `outline` ink (`#897363`, an existing token) — muted history, not hidden (the list is honest about where you have been).
---
## Maneuver Banner (D-06 — the top register's guidance occupant)
Full-bleed matte-black chassis under the safe inset. The whole banner is the list toggle (`<button>`, full-width, hit height = banner height ≥ 44px).
**Structure (single row, top→bottom within the band):**
| Slot | Class / geometry | Content |
|------|------------------|---------|
| Register line | `.text-label-sm`, `chassis-subtle` ink, gap `space-xs` | `GUIDE // NEXT` — or the state word per the state table below (instrument vocabulary, en under both locales, composed in code from the `GUIDE` base key) |
| Arrow plate | 48×48px `surface-dark` field | 32×32 inline SVG maneuver glyph, `signal-orange`, 3px stroke, square linecaps (the committed glyph set, in-app rendering) |
| Distance | `.text-telemetry-num` `pure-white` + unit in `.text-label-sm` | `250 M` / `1.2 KM` — remaining distance to the maneuver, from `cumulativeDistanceM` + snapped progress (never raw fix-to-point distance, D-07); unknown during REROUTING/RESUMING = `—` (strip's unknown-value glyph) |
| Street line | `.text-headline-md`, `soft-beige` ink, uppercase, single line, ellipsis overflow (the list carries the full name) | Street/designation verbatim from the route step `name` (data, not copy — destination-name rule); unnamed segment renders the `streetUnnamed` fallback copy |
| Toggle word | `.text-label-sm`, `chassis-subtle` ink, right edge | `LIST` when collapsed / `CLOSE` when expanded (real-word affordance — no chevron decoration); `aria-expanded` carries the state |
- **Recalculating-calm state (D-04):** register → `GUIDE // REROUTING`; street slot → the reroute copy (calm, no blame, no apology); distance → `—`; the 8px orange gantry square machine-blinks beside the copy (boot-card precedent: JS-timed two-state toggle ~500ms — an indicator duty cycle, not CSS animation). The arrow plate holds the last glyph — nothing spins, nothing blames.
- **States are instant hard swaps** (no transitions — the shell law). The banner never blanks mid-swap: every state renders all five slots.
---
## Maneuver List (D-06 — collapsible, collapsed default)
A `.card` panel in the center register (card geometry: max-width 320px, centered, `calc(100% - 2×margin)`), rendered **above** the map and below the banner while expanded; max-height 480px, inner scroll. **No dim layer, no modal chrome** — the boot-card precedent: the map stays visible and calm behind it.
- **Toggle:** the banner (above). Collapsed by default — guidance's default state is banner + card, the map dominant.
- **Tap-out collapses:** a full-viewport transparent layer beneath the panel (no dim — `background: transparent`) catches any map tap and collapses the list. Rams-clean, no overlay glass.
- **Row anatomy (top→bottom, trip order):** glyph 24×24 (same committed set, 2px stroke rendering at this size — the icon family's uniform-weight rule) · street `body-md` (verbatim, ellipsis, full name in the row's `title`) · distance `label-sm` right-aligned (`{n} M` / `{n.n} KM` remaining to that maneuver).
- **Row states:** NEXT (the banner's maneuver) = orange glyph + 8px orange square lamp at the row's left edge + on-surface ink; FUTURE = `matte-black` glyph + on-surface ink (dark theme: `inverse-on-surface`); PAST = `outline` glyph + `outline` ink.
- **Rows are real text** (readable as-is, no aria required); glyphs are `aria-hidden`; the panel is `role="region"` with `aria-label` = `ariaManeuverList`; the banner button carries `aria-expanded` + `aria-controls`.
- **Recalculating refresh (D-04):** on `ON_ROUTE` after a reroute, the list re-renders from the new route's steps — instant swap, scroll resets to top (the NEXT row is the first visible row).
---
## Guidance Card (D-06 — the route card evolved, bottom register)
The Phase 5 route card's slots evolve in place — same card class, same bottom offset (64px composite), same builder discipline (createElement + textContent only; strings module only; no per-card padding overrides — the 05 addendum's `.card` child rules carry the rhythm).
**Structure (top→bottom):**
| Slot | Class | Content |
|------|-------|---------|
| Register | `.card-header` › `.card-ref` | `GUIDE` — the route card's `ROUTE` register, evolved (instrument vocabulary, en both locales) |
| Headline | `.text-headline-md` | Destination name, verbatim — the continuity anchor holds |
| Telemetry row | flex, gap `space-xs` | Minutes remaining in `.text-telemetry-num` + `MIN` in `label-sm` (the route card's time slot, now live and deadband-smoothed, D-07) · `{km} KM` in `label-sm` · `ETA 18:42` in `label-sm` (clock = now + smoothed minutes, updated on the same deadband) |
| Progress bar | 4px, no radius | Track = `chassis-subtle` with a 1px `matte-black` border (checkbox geometry — distinguishable without color); fill = `signal-orange`, width = snapped progress %, set instantly per accepted fix (no transition). `role="progressbar"` + `aria-valuemin/max/now` + localized `aria-label` (`ariaTripProgress`) |
| State line | `.text-body-sm` | ABSENT in the normal state (less, but better). Present only in the no-reroute degradation: the honest copy below |
| Actions | `.card-actions` | One `.btn-secondary`: `END TRIP` → opens the abandon confirm card (the D-09 surface — the same confirm the BackButton and native-close paths reach). **No `.btn-primary` on the guidance card**: the drive's forward action is driving; orange CTA fill appears nowhere in guidance chrome except the reserved accents above |
**Height budget: ≤ 200px** (register + headline + telemetry + bar + one action; the state line is the only conditional row). The camera bottom padding (64) + this budget keeps the puck's corridor clear, same arithmetic family as Phase 5.
---
## Abandon-Route Confirm Card (D-09 / PX-03)
One confirm surface for ALL guidance exits: the guidance card's `END TRIP`, the BackButton from guidance, and the defense-in-depth native closing confirmation (the Rams card is the designed surface; the native dialog is the net behind it — research Open Q 2 recommendation, planner wires it). Standard card anatomy, **bottom register** (thumb-reach call — confirmations live where one-handed thumbs are; the banner is glance-only, never a decision surface).
| Element | en | ms (Bahasa Melayu) |
|---------|----|--------------------|
| Register | `GUIDE // CONFIRM` | `GUIDE // CONFIRM` (instrument vocabulary) |
| Headline | `Abandon route?` | `Tinggalkan laluan?` |
| Body | `Guidance stops and this trip's record is deleted. Nothing is stored.` | `Panduan berhenti dan rekod perjalanan ini dipadam. Tiada apa-apa disimpan.` |
| Primary CTA (`.btn-primary`) | `YES, ABANDON` | `YA, TINGGALKAN` |
| Secondary CTA (`.btn-secondary`) | `CONTINUE` | `TERUSKAN` |
- The body's deletion clause is the brand speaking (PRIV-01: the privacy promise stated once, clinically, at the exact moment it executes — D-14).
- `YES, ABANDON` is `.btn-primary` (the working forward path — Phase 3 denied-card mapping precedent). **Not red:** abandoning is deliberate, not a failure; orange = primary action semantics; the confirm card carries no red.
- While the confirm card is open, a BackButton click means CONTINUE (dismiss, stay in guidance — the no-dead-ends law applied to the confirm layer).
- Both CTAs are full component-class buttons ≥ 44px effective height; `:focus-visible` brackets are built into `.btn-*` (Phase 3).
---
## Trip-Ended Card (D-14 — PRIV-01's in-app half, visible)
Renders in the **center register** the instant the trip ends — arrival (snapped progress ≥ `ARRIVAL_THRESHOLD_M`) or abandon (confirm path). Both paths: guidance chrome (banner, list, guidance card) clears instantly, the envelope is DELETEd server-side (the KV record gone — the TTL law's promise executed actively), the watch stops (the trip is over — privacy-minimal), and the camera holds its last pose.
| Element | en | ms (Bahasa Melayu) |
|---------|----|--------------------|
| Register | `GUIDE // END` | `GUIDE // END` (instrument vocabulary) |
| Headline | `Trip ended` | `Perjalanan tamat` |
| Body | `Route guidance stopped and the trip record is deleted — nothing about this drive remains.` | `Panduan laluan dihentikan dan rekod perjalanan dipadam — tiada apa-apa tentang pemanduan ini kekal.` |
- ONE card for arrival and abandon — both delete, one message (less, but better).
- The card persists until the next action (no silent auto-dismiss — honesty law); the BackButton is visible and walks to the search root; a new destination resolution replaces it through the Phase 4/5 chain.
---
## Turn-Card Chat Message (D-12 — the minimized handoff's surface)
A Telegram chat message pushed by the Worker cron (D-11): `sendPhoto` with the maneuver glyph PNG + an HTML caption + one inline button. It is chat content, not app chrome — Telegram renders the text; the spec pins the composition. Owned strings live in the **bot** strings module (`bot/src/i18n/strings.ts` pattern; the bot copy gate asserts them), not the web module.
### The 8-glyph set (committed once, reused everywhere)
| # | Kind | Maneuver meaning | Glyph geometry (32×32 grid, 3px stroke, square caps, orthogonal/45° only) |
|---|------|------------------|---------------------------------------------------------------|
| 1 | `depart` | Trip start onto the first road | 4×4 filled square (origin-ring echo) + vertical shaft up |
| 2 | `straight` | Continue ahead | Vertical shaft + orthogonal arrowhead |
| 3 | `left` | Turn left | Up shaft + 90° left head (the mockup's turn glyph family) |
| 4 | `right` | Turn right | Mirror of `left` |
| 5 | `slight-left` | Keep left | 45° up-left shaft + head |
| 6 | `slight-right` | Keep right | Mirror of `slight-left` |
| 7 | `uturn` | U-turn (routed for real in v1 — D-01 defers turn restrictions, so the graph WILL produce them; the glyph is honest about it) | Up shaft, orthogonal 90° left, down shaft — a machined staple, no arc (orthogonal law) |
| 8 | `arrive` | Arrival | Vertical shaft ending on the 12×12 filled square + tail — the Phase 5 destination-pin glyph, promoted |
Production: hand SVGs (source of truth, committed) → `build-glyphs.mjs` (sharp) → 256×256 PNGs committed beside the bot's static assets, served from the Worker's static handler (sendPhoto fetches the public URL — research-verified). Stroke `#EB8008` on a full-bleed `#1A1A1A` plate, 256×256 output (chat renders it ~48–96px; the oversize keeps edges crisp). In-app, the SAME paths render as inline SVG at 32×32 (banner) / 24×24 (list).
### Caption layout (HTML parse mode, ≤1024 chars — verified budget; ~200 used)
```
GUIDE // T-07
<b>250 M — TURN RIGHT ONTO JALAN SULTAN ISMAIL</b>
ETA 18:42 · 7.8 KM LEFT
Best-effort projection — updates while the app is minimized.
```
| Line | Source | Rules |
|------|--------|-------|
| Register | `GUIDE // T-{nn}` — instrument constant, code-composed (nn = zero-padded maneuver index) | en both locales; plaintext |
| Maneuver line | `{dist} — {action} onto {street}` — the 8 localized action verbs below; street verbatim from the step `name` | `<b>` wrapped, uppercase (instrument voice); `streetUnnamed` fallback |
| Telemetry line | `ETA {time} · {km} KM LEFT` template — projected ETA/remaining from the envelope snapshot (the cron's projection, D-11) | plaintext; formats identical to app constants |
| Honesty line (D-13) | fixed copy | plaintext, never promises live tracking; the 8 verbs, `OPEN` button label, and both templates are localized en/ms |
**Localized action verbs (the caption's `{action}`):** `DEPART`/`MULA` · `CONTINUE ON`/`TERUS KE` · `TURN LEFT ONTO`/`BELOK KIRI KE` · `TURN RIGHT ONTO`/`BELOK KANAN KE` · `KEEP LEFT ONTO`/`SEDAKIT KIRI KE` · `KEEP RIGHT ONTO`/`SEDAKIT KANAN KE` · `MAKE A U-TURN ONTO`/`BUAT PUSARAN KE` · `ARRIVE AT`/`TIBA DI`.
**Inline button:** one button, `OPEN GUIDANCE` / `BUKA PANDUAN`, carrying the Mini App direct-link with the guidance id (research Open Q 1: `startapp`/`?g=` knob — planner picks the exact name) so a killed webview resumes from the envelope. `reply_markup` rides the same `sendPhoto` call.
**Push cadence (UX-visible half of D-11/D-13):** a card pushes when the projected next maneuver changes (or every N minutes — discretion); cron pushes pause while the app is foregrounded (structural: the envelope is DELETEd on resume, re-POSTed at minimize). Cron lag and Telegram delivery variance are documented behaviors — the copy says "best-effort", the mechanism means it.
---
## Camera Behavior (D-10)
| Event | Camera action | Parameters |
|-------|---------------|------------|
| Guidance starts | Snap to locked-follow | `easeTo` snapped position: padding `{ top: 320, bottom: 64, left: 64, right: 64 }` (center ≈65% down-screen), `pitch: 35` (the standing pitch — never changed by guidance), `zoom: 16` (named constant `GUIDANCE_ZOOM` — urban detail; replay-tunable), duration 400ms |
| Per accepted fix (≥10 m / ≥5 s, D-15) | Ease along the snapped line | `easeTo` the **snapped** progress point (D-05: one authority — the camera rides the same line the banner reads, never the raw fix), duration 400ms (`GUIDANCE_EASE_MS` — the `RECENTER_DURATION_MS` family, one easing discipline) |
| User pans/zooms | Existing `movestart` `originalEvent` rule → follow unlocks; the recenter control's notch clears (Phase 3 `free` state returns) | The route line, banner, and card are untouched — unlock is camera-only |
| Recenter tap while unlocked | Re-lock locked-follow: `easeTo` snapped point, guidance padding, notch returns | Phase 3 control semantics, guidance parameters |
| Reroute snaps in (D-04) | No camera jump — the new route's snapped line simply continues under the puck | Instant `setData` (map motion settles naturally on the next per-fix ease) |
| Trip ends | Camera holds its last pose | No move; the watch has stopped |
- **Bearing stays north-up (recorded call):** guidance never rotates the map — Phase 3/5 never touched bearing; the heading cone already communicates direction; a rotating map contradicts the calm-instrument posture. Pitch stays 35 per D-10 and the standing law.
- Camera moves are map-viewport behavior — permitted motion; the chrome never animates (Phase 3 law stands).
---
## State Coexistence — Guidance States (D-03/D-04/D-14)
One guidance surface set; states swap instantly. The Phase 5 coexistence table stands beneath this one (boot/pending/destination states are unchanged when guidance is not active).
| State | Banner | Guidance card | List | Map/camera | Strip |
|-------|--------|---------------|------|------------|-------|
| Guidance active (ON_ROUTE) | `GUIDE // NEXT`, glyph + distance + street | normal (no state line) | collapsed (toggle opens) | locked-follow, route stack as Phase 5 | compacted, lamp/word unchanged |
| OFF_ROUTE (grace window, pre-reroute) | unchanged — still `GUIDE // NEXT` (no premature signal; the machine acts, not warns) | unchanged | unchanged | locked-follow | unchanged |
| REROUTING (D-04 calm state) | `GUIDE // REROUTING`, street slot → reroute copy, distance `—`, gantry blink | unchanged (last honest numbers hold) | unchanged (stale-but-labeled) | locked-follow on old line until the new line snaps | unchanged |
| ON_ROUTE after reroute | `GUIDE // NEXT` with the new maneuver; list re-rendered | ETA/remaining continue from the new model (EMA state persists — no whiplash, D-07) | re-rendered, scrolled to top | same line-style paint, new geometry (`setData`) | unchanged |
| Router-pending degradation (graph absent, D-03 — excursion while reroute impossible) | `GUIDE // NO REROUTE`, street slot → degradation copy, distance `—` | state line carries the longer honest copy; card otherwise unchanged | unchanged | route line stays drawn (fixture/engine route is real); locked-follow continues | unchanged. **No red** — a designed deferral, not an error (router-pending precedent) |
| Resuming (re-open before first fix, D-11/D-06) | `GUIDE // RESUMING`, distance from envelope projection, gantry blink | projected numbers (labeled honest by the banner state) | collapsed | locked-follow at projected position | unchanged |
| First fix after resume | `GUIDE // NEXT` — live numbers | live numbers | — | snapped progress corrected | unchanged |
| Arrived / abandoned (D-14) | cleared instantly | → trip-ended card (center) | cleared | camera holds; watch stops | returns to full-height live strip |
| Minimize (webview `deactivated`) | — | — | — | — | envelope POSTed (best-effort, D-13); cron turn-cards begin in chat |
Guidance never starts without a route: a router-pending route state keeps the Phase 5 pending card and binds NO PX-03 protections (nothing to abandon — swipe/close stays native, BackButton shows for the pending state per Phase 5).
---
## Copywriting Contract (copy tone: clinical, honest — D-01/D-13; en + ms; web keys join `web/src/i18n/strings.ts`, bot keys join the bot strings module; never inline — the copy gates)
New `ShellStrings` keys (web side; registers and unit formats are instrument constants in both locales):
| Key | en | ms |
|-----|----|----|
| `guideRegister` | `GUIDE` | `GUIDE` |
| `guideStreetUnnamed` | `Unnamed road` | `Jalan tanpa nama` |
| `guideReroutingLine` | `Finding a new route from where you are.` | `Mencari laluan baharu dari kedudukan anda.` |
| `guideResumingLine` | `Resuming from your last known progress.` | `Sambung semula dari kemajuan terakhir anda.` |
| `guideNoRerouteLine` | `Rerouting unavailable on this server.` | `Pengiraan semula tiada pada pelayan ini.` |
| `guideNoRerouteBody` | `No road graph here — follow the drawn route; recalculation is unavailable.` | `Tiada graf jalan di sini — ikut laluan yang dilukis; pengiraan semula tidak tersedia.` |
| `ctaBannerList` | `LIST` | `SENARAI` |
| `ctaBannerClose` | `CLOSE` | `TUTUP` |
| `ctaEndTrip` | `END TRIP` | `TAMAT PERJALANAN` |
| `guideConfirmHeadline` | `Abandon route?` | `Tinggalkan laluan?` |
| `guideConfirmBody` | (confirm table above) | (confirm table above) |
| `ctaConfirmAbandon` | `YES, ABANDON` | `YA, TINGGALKAN` |
| `ctaConfirmContinue` | `CONTINUE` | `TERUSKAN` |
| `guideEndHeadline` | `Trip ended` | `Perjalanan tamat` |
| `guideEndBody` | (trip-end table above) | (trip-end table above) |
| `ariaManeuverList` | `Full maneuver list` | `Senarai penuh manuver` |
| `ariaBannerToggle` | `Next maneuver` | `Manuver seterusnya` |
| `ariaTripProgress` | `Trip progress` | `Kemajuan perjalanan` |
Bot-side keys (turn-cards): `cardAction{Depart,Straight,Left,Right,SlightLeft,SlightRight,Uturn,Arrive}` (verbs above), `cardEtaTemplate` `ETA {time} · {km} KM LEFT` / `ETA {masa} · {km} KM LAGI`, `cardHonesty` `Best-effort projection — updates while the app is minimized.` / `Unjuran cuba terbaik — dikemas kini semasa aplikasi diminimumkan.`, `cardOpenButton` `OPEN GUIDANCE` / `BUKA PANDUAN`. Register lines (`GUIDE // NEXT`, `GUIDE // T-07`, …) are instrument constants composed in code.
- **Primary CTA:** `YES, ABANDON` only (the confirm card). Orange CTA fill is otherwise absent from guidance chrome.
- **Empty state:** not applicable — guidance renders only over a live route; the pre-guidance empty states are the Phase 3/4/5 surfaces (unchanged).
- **Error state:** the no-reroute degradation IS the honest error-adjacent state (graph absent, D-03) — standard ink, no red, never blank. A reroute request failure renders the same state (fail-honest, banner stays calm).
- **Destructive actions:** exactly one — abandoning the trip. It is guarded by the confirm card on every path (card button, BackButton, native-close net). Both exit paths execute the D-14 deletion and show the trip-end card.
- **Honesty law (D-13) applied:** the turn-card never says "live", "real-time", or "guaranteed" — "best-effort projection" is the strongest claim the mechanism supports; the resuming banner labels projected numbers as resuming until a live fix lands.
**Copy gate:** the strings-module tests assert exactly these keys in en + ms (web + bot modules); no guidance copy inline in `web/src/guidance/*`, `shell.css`, or the cron handler.
---
## Transitions & Micro-Behaviors (inside Rams rules)
- **No CSS `transition`/`animation` anywhere in chrome** — unchanged law. Banner states, list expand/collapse, card swaps: instant hard swaps (no height animation on the list).
- **Machine-blink gantry** (boot-card precedent) is the only loading indicator: REROUTING and RESUMING states blink the 8px orange square at ~500ms via JS timer — two visible states, an indicator duty cycle, never an easing pulse.
- Progress bar: width set per accepted fix, instant, no transition. A bar that glides is a chart; a bar that steps is an instrument.
- Camera easing (400ms `easeTo` per fix) is map-viewport motion — permitted; chrome never moves.
- Turn-card arrival in chat: Telegram's own message rendering — no app-side animation exists or is wanted.
- No blur, no shadow, no glass, no pulse on the route line. A driven route is static geometry; only state changes it.
---
## Close Protection (PX-03 — the three layers, wired on guidance start, unwound at trip end)
| Layer | API (version guard) | Wiring |
|-------|--------------------|--------|
| Body-swipe minimize off | `disableVerticalSwipes()` (Bot API 7.7+) | On guidance start; `enableVerticalSwipes()` at trip end; capability-guarded `?.` chains per the contract style |
| Native close confirm (defense-in-depth — the header swipe cannot be disabled, official docs) | `enableClosingConfirmation()` (6.2+) | On guidance start; disabled at trip end. The Rams confirm card remains the designed surface; the native dialog is the net (research recommendation) |
| In-app exits | the abandon confirm card | `END TRIP`, BackButton-from-guidance, and any post-native-confirm return path land on the same card |
Safe areas: the banner sits at `safe-top + space-sm` (the strip's existing `--safe-top` var); the guidance/confirm cards clear `safe-bottom` via the 64px composite; insets resolve via the Phase 3 `resolveSafeInsets` union (env ∩ Telegram 8.0+, max per edge) — nothing interactive in the home-indicator zone; the basemap extends under bars.
---
## BackButton Contract (PX-01 — the full state graph, never dead-ends)
The Phase 5 wire (`wireBackButton(wa, hasOverlay, walkBack)` — predicate + handler as one change) extends to a context-aware `walkBack`:
| App state | BackButton | Click behavior |
|-----------|------------|----------------|
| Boot stages / destination card (root) | hidden (unchanged) | — |
| Route state | `show()`; click → walk back | Phase 5 walk: clears route layers, card swaps to destination card, refollow, `hide()` |
| **Guidance state** | `show()`; click → **opens the abandon confirm card** (bottom register) | Guidance is a pushed state — never torn down silently. `YES, ABANDON` → guidance teardown (chrome clears, envelope DELETE, watch stops) → **route card state** (the route still exists in memory). `CONTINUE` → card dismisses, guidance continues |
| Route state (post-abandon) | `show()`; click → walk back | The Phase 5 route walk → destination card (search root), `hide()` |
| **Trip-ended card** | `show()`; click → walk to search root | Card clears, `hide()` — the graph's exit ramp |
| Confirm card open + BackButton click | (guidance still active) | Means CONTINUE — dismiss the card, stay in guidance (no stacked dialogs) |
The full walk — search → routes → guidance → (confirm) → routes → search — presses Back at most once per state; the button is visible exactly when a pushed state exists; its handler is never a no-op; `close()` fires only at the true root (Telegram semantics). No dead ends.
---
## Haptics (D-08 — the eyes-free channel)
| Event | Cue | Trigger point | Guard |
|-------|-----|---------------|-------|
| Maneuver approach | `HapticFeedback.selectionChanged()` | Snapped progress within 300 m of the next maneuver (named constant `APPROACH_HAPTIC_M` — replay-tunable); fires once per maneuver | Bot API 6.1+ / `?.` chain — graceful no-op |
| Maneuver point | `HapticFeedback.impactOccurred("medium")` | Snapped progress crosses the step boundary (once, edge-triggered) | as above |
| Arrival (discretion extension of the same mechanism) | `HapticFeedback.notificationOccurred("success")` | `ARRIVAL_THRESHOLD_M` crossed — the trip-end card's tactile twin for eyes-on-road | as above |
No haptic on reroute (the banner stays calm — a haptic would announce blame), none on confirm dialogs (visual + thumb is enough). Every cue is once-per-edge, replay-tested with the state machine.
---
## Accessibility & Device Notes
- **Contrast (computed, WCAG):** `pure-white` on `matte-black` ≈ 17:1 (distance — the glance datum, AAA); `soft-beige` on `matte-black` ≈ 11.6:1 (street, AAA); `chassis-subtle` on `matte-black` ≈ 8.9:1 (register line, AAA); card ink `matte-black` on `soft-beige` ≈ 11.6:1 (AAA); `signal-orange` used only for graphics/fills beside ≥ AA text (arrow, lamps, bar fill — non-text UI elements ≥ 3:1 against their fields, reinforced by the bar's 1px border).
- **Thumb-reach:** destructive confirm at the bottom register (one-handed reach); banner interactions are glance + whole-band tap (no precision targets at the top); the recenter control keeps its ≥44px hit target.
- **Hit targets:** banner toggle = full band; list rows are non-interactive text (the list is a readout, not a menu — less, but better); card buttons are component-class.
- **Screen readers:** banner = one toggle button (`aria-expanded`, `aria-controls`, `aria-label`); list = labelled region of real text rows; progress bar = `progressbar` with valuetext; glyphs/lamps `aria-hidden`; state is never carried by color alone (register words, gantry, card state line are text).
- **Haptics degrade silently** (D-08); safe-area layout degrades to pure `env()` on old clients (Phase 3 law); every Telegram call is capability-guarded per the contract style (research Pitfall 5).
---
## Capture Profiles (extends `web/scripts/capture.mjs` + the Mock-Telegram harness)
New harness knob and legs; all existing legs unchanged:
| Artifact | Profile | Asserts |
|----------|---------|---------|
| Harness knob `?drive=<fixture>` | the mock's Telegram fix source replays a committed drive fixture (the D-16 replay JSON streams — corridor follow / off-route excursion / missed-turn excursion) through the live shell; pairs with `?dest=` + `?fixpair=` carriers | `ARAHARAH-GUIDE state=<state>` signals (state tokens only — no coordinates in console, D-15 hygiene) |
| `shell-guidance-active.png` | `mock=android&dest=<golden-id>&fixpair=<pair>&drive=<corridor>`, light theme | banner (`GUIDE // NEXT`, arrow, distance, street) + compact strip + guidance card + progress bar + locked camera pose |
| `shell-guidance-list.png` | same, after a scripted banner tap | list panel expanded over the map, NEXT row lamp, collapsed-by-default proof = the active leg's complement |
| `shell-guidance-rerouting.png` | `drive=<missed-turn>` captured inside the REROUTING window | calm banner state (reroute copy, `—` distance, gantry), NO red anywhere |
| `shell-guidance-confirm.png` | scripted `END TRIP` tap | confirm card in the bottom register, both CTAs, no red |
| `shell-guidance-tripend.png` | scripted `YES, ABANDON` | trip-ended card (center), guidance chrome cleared |
| `shell-guidance-dark.png` | active profile, `scheme=dark` | PX-02: chrome re-slots, banner/chassis Rams-constant |
| `guidance-gallery-compare.png` | DSGN-02 side-by-side vs `active_route_guidance/screen.png` | fidelity artifact for the phase gallery |
Turn-card captions are asserted by the bot-side suite + copy gate (`bot/test/guidance-cron.test.mjs` — caption composition, verb table, ≤1024 budget), not by browser capture.
Bundle-budget note (planner-facing): guidance chrome is DOM builders + the inline glyph paths — inside the ≤ ~200KB gzipped BA gate. The glyph PNGs ride the bot's static assets (never the web bundle); `sharp` is a maintainer devDependency only (research-verified).
---
## Fidelity Artifact (DSGN-02 obligation)
Phase exit extends the gallery pattern: the seven guidance captures above beside `active_route_guidance/screen.png`, reviewed for fidelity before phase close. Where this document, the mockup markup, and `tokens.css` disagree: `tokens.css` wins, the mockup loses, this spec records the call. Recorded calls:
- Mockup's **lane-assignment matrix, incident telemetry band, speedometer + limit medallion, telemetry gauge deck** → deferred features (incidents v2, speed limits v1.x, REQUIREMENTS deferred table) — scrubbed, nothing placeholder-shaped.
- Mockup's **REROUTE VIA TELEGRAM BOT / REPORT HAZARD / AUDIO** buttons → no such actions exist in v1 scope; guidance's only action is END TRIP.
- Mockup's **backdrop-blur header + rounded puck/medallions** → scrubbed (existing law: no blur, zero radius — the Phase 3 machined marker stands).
- Mockup's **arrow glyph at stroke 4 / 40×40 grid** → HUD spec's 32×32 / 3px wins (the committed set's geometry).
- Mockup's **56px (`w-14`) arrow plate** → 48×48 (32 glyph + 2×8 breathing; 12×8 grid).
- Mockup's orange **distance number** → `pure-white` wins (accent-singular-per-band; the arrow carries the orange).
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none (not initialized; gate N/A for the no-framework stack) | not required |
| third-party | none — glyph PNGs are committed artifacts built from committed hand SVGs via a maintainer sharp script (no registry, no CDN, no runtime dependency); map tiles remain an OpenFreeMap runtime data fetch, not a code dependency | not required |
---
## Checker Sign-Off
- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS
**Approval:** pending
---
*Phase: 06-turn-by-turn-guidance-live-driving*
*Contract drafted: 2026-10-24 by gsd-ui-researcher*
*Consumes: theme tokens (binding), liberty-rams.json layer order, 06-CONTEXT.md D-01..D-16, 05-UI-SPEC.md (binding conventions), 03-UI-SPEC.md (binding conventions), telemetry_rams/DESIGN.md §Navigation Heads-Up Display, DESIGN.md palette semantics, Phase 5 frozen GuidanceSessionEnvelope*