Files
arah2/.planning/phases/06-turn-by-turn-guidance-live-driving/06-UI-SPEC.md
T

42 KiB
Raw Blame History

phase, slug, status, shadcn_initialized, preset, created
phase slug status shadcn_initialized preset created
6 turn-by-turn-guidance-live-driving draft false none 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