Files

441 lines
42 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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*