From 4474cc26e9dc160c54e4bb86ce5a3aea05a1e884 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sun, 27 Sep 2026 19:42:06 +0700 Subject: [PATCH] feat(directions): bigger map + app-like layout and richer route details - Map is now the hero: h-[38vh] mobile / 400px desktop (was 256), with a metric Leaflet scale control. - New layout: flex column with header/modes/From/map pinned, the details as the ONLY scroll region, and Start/Stop pinned at the bottom so it's always reachable. Previously the dialog was a single overflow-hidden box, so once the map grew everything below the fold (Start, steps) was unreachable. - Summary rebuilt as stat tiles (distance / time / arrive) plus a detail line: step count, honest 'uses driving roads'/'driving estimate' notes, and the average speed the engine assumed ('21 km/h avg'; foot keeps the skate cruise estimate). Steps header shows the count; step list scrolls taller (max-h-72). - Wider dialog on desktop (sm:max-w-xl). Verified in-app: tiles + 9/8/5/5 steps per mode, follow banner still tracks movement, offline chip still appears, map 371px -> 321px with the new layout. --- AGENTS.md | 12 ++- release-notes.txt | 3 + src/components/RouteDialog.tsx | 163 ++++++++++++++++++++------------- 3 files changed, 115 insertions(+), 63 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 03a276a..c6c42f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -221,7 +221,17 @@ session with **no other memory of this project**. So: transient watch error (timeout/temporarily unavailable) must **not** tear down the watcher — only `denied` does (headless CDP testing made this obvious: moving the mocked position emits a transient error that recovered). - Still deliberately absent: voice guidance and re-routing if you leave the route. **Transit** mode uses **Transitous** (`api.transitous.org`, + Still deliberately absent: voice guidance and re-routing if you leave the route. + - **Dialog layout (2026-09-27):** app-like flex column — + `flex max-h-[calc(100dvh-2rem)] flex-col overflow-hidden`: header, mode row, + "From" row and the **map** are `shrink-0` (map `h-[38vh] sm:h-[400px]`), the + middle (stat tiles + steps) is the only scroll region (`min-h-0 flex-1 + overflow-y-auto`), and the **Start/Stop bar is pinned** at the bottom so it's + always reachable. ⚠️ Don't go back to a single `overflow-hidden` content box: + that made everything below the fold **unreachable** once the map grew. The + summary is three `StatTile`s (distance / time / arrive) + a detail line + (steps, driving-roads note, avg speed) and the map carries a metric + `L.control.scale`. **Transit** mode uses **Transitous** (`api.transitous.org`, MOTIS 2, open CORS) — note origin/destination go to that service, unlike road modes. **No external map-app handoff anywhere** (product decision) and no scooter/moped profile. **Speed (2026-09-27):** `RouteDialog` seeds origin from diff --git a/release-notes.txt b/release-notes.txt index 4e1baba..0d50ba4 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -1,6 +1,9 @@ v1.5.3 🧭 Real navigation, not just a route preview +- A much bigger map (with a metric scale bar), and the plan now reads at a + glance: distance, time and arrival time as clear stats, plus step count and + the average speed the route assumes. Start/Stop stays pinned at the bottom. - Travel modes finally give genuinely different answers: walking 13 min, biking 5, driving 2 on the same trip. Before, every mode showed the same number. - Route steps — the actual turns and street names. Tap a step to jump to it on diff --git a/src/components/RouteDialog.tsx b/src/components/RouteDialog.tsx index 893b6c5..b4ef27d 100644 --- a/src/components/RouteDialog.tsx +++ b/src/components/RouteDialog.tsx @@ -94,6 +94,27 @@ interface RouteDialogProps { * maps app is opened — the route comes from the keyless public OSRM server and * is drawn here. See src/lib/routing.ts for the honest scope notes. */ +/** Big number + label tile used in the route summary. */ +function StatTile({ label, value, sub }: { label: string; value: string; sub?: string }) { + return ( +
+

{value}

+

+ {label} +

+ {sub &&

{sub}

} +
+ ); +} + +/** The average speed the routing engine assumed for this mode, e.g. "21 km/h avg". */ +function speedLabel(distanceM: number, durationS: number): string | undefined { + if (!(distanceM > 0) || !(durationS > 0)) return undefined; + const kmh = distanceM / 1000 / (durationS / 3600); + if (!Number.isFinite(kmh) || kmh <= 0) return undefined; + return `${kmh < 10 ? kmh.toFixed(1) : Math.round(kmh)} km/h avg`; +} + /** Local clock time you'd arrive if you set off now. */ function etaClock(durationS: number): string { return new Date(Date.now() + durationS * 1000).toLocaleTimeString([], { @@ -322,6 +343,9 @@ export function RouteDialog({ className: 'skatespots-dark-tiles', }).addTo(map); + // Metric scale bar — genuinely useful on a route map (how long is that line?). + L.control.scale({ imperial: false, position: 'topleft' }).addTo(map); + // Destination marker const destIcon = L.divIcon({ className: 'route-dest-icon', @@ -489,8 +513,10 @@ export function RouteDialog({ return ( - - + {/* App-like shell: header + modes + map are pinned, the details in the + middle scroll, and the Start/Stop bar stays reachable at the bottom. */} + + Route to {destinationLabel} @@ -503,7 +529,7 @@ export function RouteDialog({ {/* Mode toggle */} -
+
{TRAVEL_MODES.map((m) => ( - ) : ( - - )} -

- {following - ? 'Following your position and the next turn. No voice guidance.' - : 'Follows your position and shows the next turn as you roll.'} -

-
- )} - {/* Turn-by-turn orientation — a static list. Honestly NOT live navigation. */} {mode !== 'transit' && route?.steps && route.steps.length > 1 && (
-

+

Route steps + + {route.steps.length} +

{mode === 'foot' && (

walking route — quicker on a board

)}
-
    +
      {route.steps.map((s, i) => (
)} + +
{/* /scrollable details */} + + {/* Follow mode: explicit, and honest that there's no voice guidance. */} + {route && !route.transit && ( +
+ {following ? ( + + ) : ( + + )} +

+ {following + ? 'Following your position and the next turn. No voice guidance.' + : 'Follows your position and shows the next turn as you roll.'} +

+
+ )} +
);