docs(05): UI-SPEC approved — checker flags folded (pin tail 8px, exception-class wording, addLayer precision)
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
---
|
||||
phase: 5
|
||||
slug: routing-route-display
|
||||
status: approved
|
||||
reviewed_at: 2026-10-24 (checker: APPROVED, 2 spacing flags + 1 wording note folded)
|
||||
shadcn_initialized: false
|
||||
preset: none
|
||||
created: 2026-10-24
|
||||
---
|
||||
|
||||
# Phase 5 — UI Design Contract (Routing + Route Display)
|
||||
|
||||
> Visual and interaction contract for the route experience. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
|
||||
>
|
||||
> **Source posture:** the design system already exists and is IN USE — `theme/dist/rams.css` + tokens, and the live shell (strip, card stack, recenter control, gantry, destination card) shipped in Phase 3 and passed its own UI-SPEC + device gates. This contract specifies **extension** of that shell: route line layers, the alternatives card + chips surface, the router-pending state, and camera behaviors. It invents no new token values, no new component vocabulary, no new colors. Binding upstream: 05-CONTEXT.md D-01..D-12 (fixture backend, router-pending state, bottom card + chips, orange casing, display-only terminal), the approved 03-UI-SPEC (geometry tables, theme mapping, strip/card/control components, copy discipline, a11y rules — ITS CONVENTIONS BIND), 04-CONTEXT.md (destination hand-off), Phase 1 D-08 (orange = navigation semantics only). The `navigate_map_overview` mockup is layout reference only; rounded/blur/shadow residue is scrubbed as before.
|
||||
>
|
||||
> **Map-law restated (05-CONTEXT canonical refs):** the MapLibre style is never re-set. Route rendering is `addLayer` + `setData` on sources added once — never re-set; additions only (`addLayer`/`addSource`/`setData`) — zero style re-creation.
|
||||
|
||||
---
|
||||
|
||||
## 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 (unchanged; the destination pin glyph joins this family) |
|
||||
| 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):
|
||||
|
||||
| Class | Used for in Phase 5 |
|
||||
|-------|---------------------|
|
||||
| `.card`, `.card-header`, `.card-ref` | Route card + router-pending card (same builders' discipline as `ui/cards.ts` / `search/destination-card.ts`) |
|
||||
| `.btn-secondary` | SEARCH AGAIN affordance (route card + pending card) — **deliberately not `.btn-primary`**: the route state has no forward action (D-10 forbids a start-guidance button), so no orange CTA fill appears in any Phase 5 surface |
|
||||
| `.text-headline-md`, `.text-body-md`, `.text-body-sm`, `.text-label-sm`, `.text-telemetry-num` | Typography helpers (telemetry-num's sanctioned arrival — see Typography) |
|
||||
|
||||
New CSS for this phase (route card body, chips, marker chrome) lives in `shell.css` and references `var(--color-*)` / `var(--spacing-*)` / `var(--type-*)` only. The machined-geometry literal list grows by exactly: the chip plate (visible 28px tall, hit-slop to 44), the origin ring (24px, 2px stroke), the destination pin glyph (12px square + 8px tail), and the route line widths (map-space px, table below) — 4-multiples, 1px/2px rules, machined glyph geometry, and map-space paint arithmetic (line widths 9/5/3) — the established exception classes.
|
||||
|
||||
---
|
||||
|
||||
## Shell Layout — Register Bands (extended, D-08)
|
||||
|
||||
The Phase 3 partition is unchanged. Phase 5 populates the bottom register persistently for the first time:
|
||||
|
||||
| Band | Content in Phase 5 | Geometry |
|
||||
|------|--------------------|----------|
|
||||
| Top register | Status strip — **unchanged** (location register only; see Strip note below) | as Phase 3 |
|
||||
| Center register | Rams basemap + position marker + **route line layers + origin ring + destination pin** (map content); boot/denied/pending/destination cards (card stack, center placement) | Map stays the dominant surface — the route is drawn ON it, never as a panel |
|
||||
| Bottom register | **Route card (persistent, D-08)**; signal-lost card (existing, transient); recenter control bottom-right; attribution chip bottom-left | Route card clears the recenter control — see exact offset below |
|
||||
|
||||
- **Status strip: NO new states.** The strip marks the session/location state machine (Phase 3 table stands). Route calculation is a local fixture resolve — no waiting surface, no strip lamp change. The router-pending state is a *routing* condition, not a location degradation: it gets **no** red lamp, no strip entry. The route's register is its card.
|
||||
- **No new controls** — no layer toggles, no start-guidance button, no progress UI (D-10). The recenter control keeps its Phase 3 behavior exactly (it recenters on the user; the route stays drawn regardless of camera).
|
||||
- Route card placement: bottom register, `bottom: calc(var(--safe-bottom) + 64px)` — the **64px control-clearance composite** = `space-sm` (8, the control's own bottom offset) + 40px (machined control plate) + `space-md` (16, breathing). Card max-width 320px unchanged; on a 390×844 viewport the card never intersects the control (card bottom edge sits above the control's top edge).
|
||||
- Attribution chip stays flush bottom-left, below the route card's bottom edge — never covered.
|
||||
|
||||
---
|
||||
|
||||
## Spacing Scale
|
||||
|
||||
Bound to tokens (`--spacing-*`), consumption not invention. Phase 3 table stands; Phase 5 adds these exact numbers:
|
||||
|
||||
| Element | Number | Token source |
|
||||
|---------|--------|--------------|
|
||||
| Route card bottom offset | 64px = `space-sm` + 40px machined + `space-md` (composite, documented) | tokens + machined control |
|
||||
| Route card padding | `space-md space-md` (16/16); actions block top padding `space-sm` | tokens (unchanged pattern) |
|
||||
| Chip padding | `space-xs space-sm` (4/8) → visible height 28px (label-sm 12px line + 2×8) | tokens |
|
||||
| Chip hit target | ≥ 44×44px via `::after { inset: -8px }` transparent slop (28+16=44) | a11y exception (4-multiple, Phase 3 precedent) |
|
||||
| Chip row gap | `space-xs` (4); chips never wrap (2 max, one row) | tokens |
|
||||
| Telemetry row internal gap | `space-xs` (4) | tokens |
|
||||
| Origin ring | 24px diameter, 2px `matte-black` stroke, no fill | machined geometry |
|
||||
| Destination pin glyph | 12×12px square + 8px triangular tail (20px total height); name chip above it, offset `space-sm` | machined geometry |
|
||||
| Camera fit-bounds padding | `{ top: 96, bottom: 320, left: 64, right: 64 }` — map-space px (MapLibre padding, not CSS; same class as Phase 3's 96px = 12×`space-sm`). Bottom 320 = 64 offset + ≤240 card height budget + 16 slack | spec geometry (8-multiples) |
|
||||
|
||||
Exceptions: none beyond the 44px chip hit target and the map-space camera/line arithmetic (both established exception classes).
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
The Phase 3 scale stands. `telemetry-num` joins exactly as 03-UI-SPEC anticipated ("not used this phase — Phase 5/6"): **the route time is its first consumer.** Zero new sizes, zero new weights.
|
||||
|
||||
| Role | Token | Spec | Usage in Phase 5 |
|
||||
|------|-------|------|------------------|
|
||||
| Route time | `--type-telemetry-num` | 28px Space Grotesk 700 / 32px, tracking −0.03em | Route card time counter (`22`), the card's data point |
|
||||
| Card headline | `--type-headline-md` | 20px Space Grotesk 600 / 26px, tracking −0.02em | Destination name (verbatim gazetteer name — Phase 4 display rule); pending-card headline |
|
||||
| Card body | `--type-body-md` | 14px Geist 400 / 20px | Pending-card body |
|
||||
| Difference / hint | `--type-body-sm` | 12px Geist 400 / 16px | One-line difference vs best; pending-card hint |
|
||||
| Chips / units / register | `--type-label-sm` | 10px Geist 700 / 12px, tracking +0.08em, uppercase | Chip text, `MIN`/`KM` unit words, card-header registers, pin name chip |
|
||||
| CTA label | `--type-label-lg` (inside `.btn-secondary`) | 14px Space Grotesk 700 / 18px, uppercase | SEARCH AGAIN |
|
||||
|
||||
Unit formats are instrument constants, code-composed like Phase 3's `±12 M` telemetry: time `22 MIN` (integer minutes), distance `8.4 KM` (one decimal), chip `{nn} // {min} MIN · {km} KM`. Units unchanged under `ms` (Phase 3 precedent). Body line-height ~1.43, heading 1.3 — token ratios verbatim.
|
||||
|
||||
---
|
||||
|
||||
## Color
|
||||
|
||||
60 / 30 / 10 split unchanged. **The route line becomes signal-orange's primary consumer — the Phase 1 reservation fulfilled.** The reserved-for list is restated and extended:
|
||||
|
||||
| Role | Value | Usage in Phase 5 |
|
||||
|------|-------|------------------|
|
||||
| Dominant (60%) | Rams basemap grayscale + shell chrome (unchanged) | The route is drawn over the grayscale world; nothing else competes |
|
||||
| Secondary (30%) | Instrument surfaces (unchanged) | Route card + pending card: light `soft-beige` / dark `inverse-surface` via the existing theme slots; chips idle = `matte-black` plate |
|
||||
| Accent (10%) | `signal-orange` `#EB8008` | **Reserved-for list (exact, Phase 5):** the route center line (selected route only — the primary consumer); position-marker heading cone; follow-active recenter notch; focus brackets (chips inherit the 3px orange focus rule); boot lamp + gantry; primary CTA fill on Phase 3 state cards (denied card — unchanged); `:active` press inverts (chips, recenter — existing law); strip boot-stage lamps. |
|
||||
| Destructive | `functional-red` `#ED3F1C` | **Unchanged and NOT extended:** denied/signal-lost lamps only. The router-pending state is a documented deferral, not an error — it renders in standard card ink with no red anywhere. |
|
||||
| Operational | `olive-green` `#736B1E` | LIVE lamp only (unchanged) |
|
||||
|
||||
Route-line paint (the D-09 contract):
|
||||
|
||||
| Layer (bottom→top) | Color | Width | Opacity | Geometry |
|
||||
|--------------------|-------|-------|---------|----------|
|
||||
| `route-casing` (selected) | `matte-black` `#1A1A1A` | 9px | 1.0 | line-cap `square`, line-join `bevel` (bevel: no miter spikes on sharp OSM angles; square caps match Rams glyph caps) |
|
||||
| `route-line` (selected) | `signal-orange` `#EB8008` | 5px | 1.0 | same caps/joins; **solid — the mockup's dashed centerline is decorative residue, scrubbed** |
|
||||
| `route-alt-casing` (unselected) | `matte-black` `#1A1A1A` | 5px | 1.0 | half-weight echo of the selected stack |
|
||||
| `route-alt-line` (unselected) | `soft-beige` `#D9D2C6` | 3px | 1.0 | reads as the basemap's own road language (dark casing + light fill): quiet, route-shaped, zero accent competition |
|
||||
|
||||
- **Opacity 1.0 everywhere** — physical planarity law: surfaces are opaque; a translucent route is haze, not an instrument.
|
||||
- **Unselected treatment:** the beige/black mini-stack. Selected vs unselected differ by weight AND the accent — orange appears exactly once on the map, on the chosen route. Chip tap swaps geometries between the selected and alt source pairs (`setData`, instant); layers themselves are static.
|
||||
- **Layer ordering:** all four layers added inside the existing `map.on("load")` handler, AFTER the `building-3d` block, each `addLayer` with no `beforeId` → order bottom→top: `route-alt-casing`, `route-alt-line`, `route-casing`, `route-line`. Result: the route paints above every basemap layer **including building-3d** — at pitch 35 the extrusions never clip the product's one accent, and labels/shields are already beneath building-3d (existing Phase 1/3 condition, not ours to reorder). DOM markers (position marker, origin ring, destination pin) always paint above all canvas layers.
|
||||
- **DSGN-01 audit note:** MapLibre paint cannot consume CSS variables — the three hex values are copied verbatim from `tokens.css` as build-time constants (the `building-3d` precedent: "zero invented colors"); the Phase 1 off-token sweep pattern covers them.
|
||||
- Sources: one GeoJSON source per role — `route-alt` and `route` (or four sources if the executor prefers one per layer) — added empty at load, filled/cleared via `setData`. Sources are data, not style: this is not style mutation.
|
||||
|
||||
---
|
||||
|
||||
## Origin Ring + Destination Pin (map furniture, D-09)
|
||||
|
||||
| Element | Geometry | Color / ink | A11y |
|
||||
|---------|----------|-------------|------|
|
||||
| Origin ring | 24px diameter circle, 2px solid `matte-black` stroke, **no fill**, centered on the route geometry's first vertex — the route's actual origin, which may sit meters from the live fix (fixture world). The position marker (dot + cone + accuracy circle) renders unchanged beneath it | `matte-black`, subdued by design — no accent, no fill | decorative, `aria-hidden` |
|
||||
| Destination pin glyph | 12×12px `matte-black` filled square + 3px triangular tail pointing down at the destination coordinate (square linecaps, orthogonal family — the recenter-glyph's sibling). **Never orange** — the pin is venue semantics, orange is route-only (D-09) | `matte-black` fill, 1px `pure-white` inset rule (reads on dark map tones) | glyph `aria-hidden` |
|
||||
| Destination name chip | `matte-black` plate above the pin, offset `space-sm`; `pure-white` `label-sm` uppercase text = venue name verbatim; single line, max-width 160px, ellipsis overflow (the card carries the full name) | instrument-plate precedent (attribution chip, `card-ref`) | real text — readable as-is, no aria needed |
|
||||
| Endpoint join | route line terminates **under** the pin; origin line starts **inside** the ring — no floating line ends | — | — |
|
||||
|
||||
---
|
||||
|
||||
## Route Card + Alternatives Chips (D-08, exact geometry)
|
||||
|
||||
The card is the selected route's surface; the chips are the alternates. Built via the existing card-stack `showCard(card, "bottom")` mechanism — one card visible at a time, instant swaps.
|
||||
|
||||
**Structure (top→bottom):**
|
||||
|
||||
| Slot | Class | Content |
|
||||
|------|-------|---------|
|
||||
| Register | `.card-header` › `.card-ref` | `ROUTE` — or `ROUTE {n}/{N}` when alternatives exist (selected index of count). Instrument vocabulary, en under both locales |
|
||||
| Headline | `.text-headline-md` | Destination name, verbatim (the continuity anchor from the destination card) |
|
||||
| Telemetry row | flex, gap `space-xs` | Time number in `.text-telemetry-num` + `MIN` in `label-sm`; distance `8.4 KM` in `label-sm`, baseline-aligned |
|
||||
| Difference line | `.text-body-sm` | One-line difference vs best (copy table below) |
|
||||
| Chips row | `role="group"`, flex, gap `space-xs` | Up to 2 alternate chips (N−1 when N routes; single-route draws render no chip row at all) |
|
||||
| Actions | `.card-actions` | One `.btn-secondary`: SEARCH AGAIN (the D-10 restart affordance) |
|
||||
|
||||
**Height budget: ≤ 240px** (content contract enforces it: one-line register, one-line headline, one row of chips). This budget is what the 320px camera bottom padding derives from.
|
||||
|
||||
**Chip contract:**
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Element | `<button type="button">` — never a div-with-onclick |
|
||||
| Visible plate | `matte-black` background, `pure-white` `label-sm` uppercase text, padding 4/8, visible height 28px, zero radius |
|
||||
| Text | `{nn} // {min} MIN · {km} KM` (e.g. `02 // 24 MIN · 9.1 KM`) — one line, fixed format, never truncates |
|
||||
| Hit target | ≥ 44×44px via `::after { inset: -8px }` transparent slop |
|
||||
| Press (`:active`) | instant invert to `signal-orange` field + `matte-black` text (mockup pill precedent, tactile-depression law) — transient press feedback only; **there is no persistent "selected chip" state**: the selected route lives in the card, the tapped route becomes the card, the previously-selected becomes a chip. Chip count is invariant (N−1) |
|
||||
| Focus (`:focus-visible`) | 3px `signal-orange` bracket (`.toggle-option` precedent) |
|
||||
| Selection behavior | tap → geometries swap between selected/alt sources (instant `setData`) → card re-renders with the new route's time/distance/difference → camera refits the new route's bounds (400ms, below). No easing, no crossfade |
|
||||
| A11y | `aria-label` = clean localized form (`ariaRouteChip` template — copy table); the group carries `aria-label` (`ariaRouteAlternatives`) |
|
||||
|
||||
**Map is not the a11y surface for route data:** the line layers and ring/pin are canvas/DOM decoration; the card is the semantic statement of the route (time, distance, difference, name). Chips are the only interactive route elements.
|
||||
|
||||
---
|
||||
|
||||
## Camera Behavior (D-09)
|
||||
|
||||
| Event | Camera action | Parameters |
|
||||
|-------|---------------|------------|
|
||||
| Route drawn (auto: live fix + resolved destination → fixture resolves) | `fitBounds` over the selected route geometry | padding `{ top: 96, bottom: 320, left: 64, right: 64 }`, duration 400ms, default easing family — identical discipline to `follow.ts` (`CAMERA_PADDING_PX`/`RECENTER_DURATION_MS` constants; the route adds its own padded constants beside them) |
|
||||
| Chip select (alternative chosen) | `fitBounds` over the newly selected geometry — **always refits** (D-10: selection redraws + camera refits), idempotent when bounds are near-identical | same parameters |
|
||||
| SEARCH AGAIN | clears layers, returns camera to the user: `flyTo` latest fix via the existing follow controller, `following` resumes | Phase 3 recenter parameters (96px, 400ms) |
|
||||
| User pans/zooms during route display | existing `movestart` rule → `free` mode; the route stays drawn | no change |
|
||||
| Recenter tap during route display | recenters on user; route stays drawn | no change |
|
||||
| Pitch/bearing | **untouched by fitBounds** — center+zoom only; pitch stays 35, bearing stays as the user left it | — |
|
||||
|
||||
Camera moves are map-viewport behavior — permitted motion; the chrome never animates (Phase 3 Micro-Behaviors law stands).
|
||||
|
||||
---
|
||||
|
||||
## State Coexistence — Destination Card ↔ Route Card (04 hand-off)
|
||||
|
||||
The card stack renders exactly one card per state; transitions are instant hard swaps. Route calculation triggers **automatically** — no route button exists (D-10): once the location chain is live AND the destination resolved, the fixture `RoutingSource` is consulted.
|
||||
|
||||
| App state | Center register | Bottom register | Map layers | Camera |
|
||||
|-----------|-----------------|-----------------|------------|--------|
|
||||
| Live, destination resolved, route not yet consulted | destination card (Phase 4, unchanged) | — | empty | follow user |
|
||||
| Live + route resolved | *(empty — the map is the surface)* | **route card** | selected stack + alt mini-stacks + ring + pin | fitBounds route |
|
||||
| Live + route pending (D-03) | **router-pending card** | — | empty (nothing fake is ever drawn) | follow user (unchanged) |
|
||||
| Destination unavailable (Phase 4 card) | unavailable card | — | empty | follow user — **no route request is ever made** |
|
||||
| No destination (`?dest` absent) | *(Phase 3 behavior byte-identical)* | — | empty | follow user — no route request |
|
||||
| Location stale during route display | — | **route card persists** | route stays drawn; position marker dims 40%, strip lamp red (Phase 3 signals) | no refit |
|
||||
| Location stale during pending | pending card | signal-lost card (coexists — different registers) | empty | no change |
|
||||
|
||||
Staleness never evicts the route card: the drawn route is static fixture data; staleness is a *live-position* condition and keeps its Phase 3 signals (dimmed marker, red lamp). Evicting the terminal surface for a location note would break D-10.
|
||||
|
||||
---
|
||||
|
||||
## Router-Pending Card (D-03 — the denied-card class, full copy table)
|
||||
|
||||
A deliberate Rams surface for arbitrary origins/destinations the fixture corpus does not cover. Same anatomy as the denied card: register, headline, body, hint, CTA. Center register. Clinical, honest, calm — the app states that routing isn't wired for this trip, offers the way back, and never apologizes theatrically. **Never** a blank map, never a fake route, never red (it is a deferral, not a failure).
|
||||
|
||||
| Element | en | ms (Bahasa Melayu) |
|
||||
|---------|----|--------------------|
|
||||
| Register | `ROUTE // ROUTER` | `ROUTE // ROUTER` (instrument vocabulary) |
|
||||
| Headline | `Routing not wired up yet` | `Penghalaan belum dipasang` |
|
||||
| Body | `Routes currently cover known KL landmarks only. The general router ships later — your coordinates were never sent anywhere.` | `Laluan buat masa ini meliputi mercu tanda KL yang diketahui sahaja. Penghala am akan datang kemudian — koordinat anda tidak pernah dihantar ke mana-mana.` |
|
||||
| Hint (body-sm) | `Send /cari in the chat and pick another landmark.` | `Hantar /cari dalam chat dan pilih mercu tanda lain.` |
|
||||
| CTA (`.btn-secondary`) | `SEARCH AGAIN` | `CARI SEMULA` |
|
||||
|
||||
The body's privacy clause is the brand speaking (D-01: origin+destination never go to a third party — the fixture world makes this literally true, and the card says so once, without preaching).
|
||||
|
||||
---
|
||||
|
||||
## Copywriting Contract (copy tone: clinical, honest — D-01; en + ms; all keys join `web/src/i18n/strings.ts`, never inline — COPY5 gate)
|
||||
|
||||
New `ShellStrings` keys (compose with the Phase 3 keys; register labels and unit formats are instrument constants in both locales):
|
||||
|
||||
| Key | en | ms |
|
||||
|-----|----|----|
|
||||
| `routeRegister` | `ROUTE` | `ROUTE` |
|
||||
| `routeDiffFastest` | `Fastest route` | `Laluan terpantas` |
|
||||
| `routeDiffSlower` (template) | `+{min} MIN VS ROUTE {n}` | `+{min} MIN VS LALUAN {n}` |
|
||||
| `routePendingRegister` | `ROUTE // ROUTER` | `ROUTE // ROUTER` |
|
||||
| `routePendingHeadline` | `Routing not wired up yet` | `Penghalaan belum dipasang` |
|
||||
| `routePendingBody` | (table above) | (table above) |
|
||||
| `routePendingHint` | (table above) | (table above) |
|
||||
| `ctaSearchAgain` | `SEARCH AGAIN` | `CARI SEMULA` |
|
||||
| `ariaRouteAlternatives` | `Alternative routes` | `Laluan alternatif` |
|
||||
| `ariaRouteChip` (template) | `Route {n}: {min} minutes, {km} kilometres` | `Laluan {n}: {min} minit, {km} kilometer` |
|
||||
|
||||
Copy formats composed in code (Phase 3 telemetry precedent, not strings): time `{n} MIN`, distance `{n.n} KM`, chip `{nn} // {min} MIN · {km} KM`, register `ROUTE {n}/{N}`.
|
||||
|
||||
- **Primary CTA:** none this phase — deliberate (D-10). The strongest affordance is `.btn-secondary` SEARCH AGAIN; orange CTA fill is absent from every Phase 5 surface.
|
||||
- **Empty state:** the empty map with no route is *not* an empty state — it is the Phase 3/4 live state (unchanged). The only new "nothing to show" surface is the pending card, fully specified above.
|
||||
- **Error state:** router-pending IS the honest error-adjacent state; fixture resolves cannot fail at runtime (committed geometry + tolerance gates). A route-source exception renders the pending card (fail-honest, never blank, never a half-drawn line).
|
||||
- **Destructive actions: none.** SEARCH AGAIN discards nothing but the view (routes are in-memory; nothing persists to delete — D-12). No confirmation surfaces.
|
||||
|
||||
**Copy gate:** the strings-module test asserts exactly these keys in en + ms; no route copy inline in `web/src/route/*` or `shell.css`.
|
||||
|
||||
---
|
||||
|
||||
## Transitions & Micro-Behaviors (inside Rams rules)
|
||||
|
||||
- **No CSS `transition`/`animation` anywhere in chrome** — unchanged law. Destination card → route card, route card → chip-swap re-render, route → pending: instant hard swaps.
|
||||
- Route draw appears as: layers populated (`setData`) + camera `fitBounds` in the same tick — the line arrives with the camera glide (map motion, permitted), chrome never moves.
|
||||
- Chip select: instant geometry/card swap + 400ms camera refit. Press invert on the chip is instant (`:active`), no transition.
|
||||
- SEARCH AGAIN: sources emptied + markers removed + card swap + `flyTo` user, all instant except the camera glide.
|
||||
- No blur, no shadow, no glass, no pulse on the route line. A drawn route is static; only selection changes it.
|
||||
|
||||
---
|
||||
|
||||
## BackButton Contract (Phase 5 — first real two-state walk, PX-01 groundwork)
|
||||
|
||||
Phase 3's single-state wire (`overlayPresent() => false` → `close()`) extends. The route state is the app's first pushed state; the walk-back is the same total function SEARCH AGAIN invokes.
|
||||
|
||||
| App state | BackButton | Click behavior |
|
||||
|-----------|------------|----------------|
|
||||
| Boot stages | `hide()` (unchanged) | — |
|
||||
| Destination card (root state) | hidden — Telegram-native swipe-down close remains the exit | — |
|
||||
| **Route state** | `show()`; `onClick` → walk back | clears route layers + ring + pin, card swaps to destination card, camera refollows user, `hide()` |
|
||||
| **Router-pending state** | `show()`; `onClick` → walk back | card swaps to destination card, `hide()` (no layers to clear) |
|
||||
| Destination-unavailable | hidden (root-equivalent; nothing pushed) | — |
|
||||
|
||||
No dead ends: the button is visible exactly when a pushed state exists, and its handler always walks to the root — never a no-op, never `close()` from a pushed state. The `overlayPresent` predicate becomes "route or pending state active"; the handler and predicate ship as one change so Phase 6 extends the graph rather than reinventing the wire.
|
||||
|
||||
---
|
||||
|
||||
## 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 `?fixpair=<pair-id>` | the mock's Telegram fix delivers the fixture pair's **pinned public origin coordinate** (fixtures are build-time public data — no user coordinate involved, D-12) | pairs with the existing `?dest=<golden-id>` carrier |
|
||||
| `shell-route-fixture.png` | `mock=android&dest=<golden-id>&fixpair=<pair>`, light theme: route drawn, route card, alt chip(s), ring + pin | `ARAHARAH-ROUTE state=drawn` signal (state token only — no coordinates in console, D-15 hygiene) + DOM check on rendered card text |
|
||||
| `shell-route-alt.png` | same, after a scripted chip tap | selected geometry swapped: orange line follows the alt corridor; card re-rendered; `ROUTE {n}/{N}` register updated |
|
||||
| `shell-route-pending.png` | `mock=android&dest=<supported-id-outside-corpus>&fixpair=<pair>` | `ARAHARAH-ROUTE state=pending` + pending card visible |
|
||||
| `shell-route-dark.png` | route-fixture profile, `scheme=dark` | PX-02: chrome re-slots, route + basemap Rams-constant |
|
||||
| `route-gallery-compare.png` | DSGN-02 side-by-side vs `navigate_map_overview/screen.png` (route visible in both columns) | fidelity artifact for the phase gallery |
|
||||
|
||||
Bundle-budget note (D-11, planner-facing): the fixture geometry + route-model code joins the web bundle under the ≤ ~200KB gzipped BA gate — the capture harness is not the enforcement point, but the route legs must run against the production-shaped bundle (harness build, same as Phase 3).
|
||||
|
||||
---
|
||||
|
||||
## Fidelity Artifact (DSGN-02 obligation)
|
||||
|
||||
Phase exit extends the Phase 1/3 gallery pattern: the five route captures above beside `navigate_map_overview/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 dashed route centerline → scrubbed (solid orange per D-09); mockup's 9×36px control plates → existing 40×40 machined control (unchanged); mockup's telemetry-deck/speedometer furniture → Phase 6+ scope, not this phase (D-10 display-only).
|
||||
|
||||
---
|
||||
|
||||
## 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 — fixtures ride in the web bundle; 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: 05-routing-route-display*
|
||||
*Contract drafted: 2026-10-24 by gsd-ui-researcher*
|
||||
*Consumes: theme tokens (binding), liberty-rams.json layer order, 05-CONTEXT.md D-01..D-12, 03-UI-SPEC.md (binding conventions), 04-CONTEXT.md (destination hand-off), Phase 1 D-08 orange reservation, telemetry_rams/DESIGN.md*
|
||||
Reference in New Issue
Block a user