feat: make route map primary and localize voice guidance
This commit is contained in:
parent
31f2858edc
commit
7254b5d763
2 changed files with 46 additions and 16 deletions
25
AGENTS.md
25
AGENTS.md
|
|
@ -750,18 +750,23 @@ session with **no other memory of this project**. So:
|
|||
it** (it's the point of the screen), then the **mode/vehicle row**, then the
|
||||
**"From" row**, then the scrolling details, with the **Start/Stop bar
|
||||
pinned** at the bottom. The map is `shrink-0` at
|
||||
`h-[34vh] min-h-[220px] sm:h-[400px]`, or `h-[54vh] min-h-[320px]`/`sm:h-[60vh]`
|
||||
`h-[56vh] min-h-[300px] sm:h-[60vh]`, or `h-[60vh] min-h-[340px]`/`sm:h-[66vh]`
|
||||
while following. The **summary is a compact one-line strip**
|
||||
(distance · time · arrive) — `StatTile` was **removed**; the three ~90px
|
||||
tiles were eating the map's space. **Owner feedback (2026-10-01): the map
|
||||
dominated the phone screen** — it was `50vh` (and `38vh` before that); `34vh`
|
||||
gives the itinerary and controls room while staying pannable, and follow mode
|
||||
restores a bigger map. The dialog's tagline ("In-app route · OpenStreetMap
|
||||
routing — nothing leaves to Google or Apple Maps") was **removed from the
|
||||
visible chrome** (kept `sr-only` for a11y); that promise now lives on
|
||||
**/privacy → "What leaves your device" → Directions**. ⚠️ Don't go back to a single
|
||||
`overflow-hidden` content box: that made everything below the fold
|
||||
**unreachable** once the map grew. The map carries a metric
|
||||
tiles were eating the map's space. **Owner feedback (2026-11-02): the map
|
||||
should be the main thing**; the map grew from `34vh` to `56vh` and the
|
||||
itinerary/details remain in the scrollable `min-h-0 flex-1 overflow-y-auto`
|
||||
region. This supersedes the 2026-10-01 “34vh gives the itinerary room”
|
||||
decision; keep the details scroller, don't make the content box one
|
||||
`overflow-hidden` block. **Navigation voice (2026-11-02):** TTS follows the
|
||||
**app language** (`dateLocaleCode()`, covering all 24 locales), not
|
||||
`navigator.language`; it prefers an installed local voice for the exact
|
||||
BCP-47 code, then its primary language, and sets `utter.lang` as fallback.
|
||||
Route prompts already use translated `routeUi.*` keys. The dialog's
|
||||
tagline (“In-app route · OpenStreetMap routing — nothing leaves to Google
|
||||
or Apple Maps”) remains **removed from the visible chrome** (kept `sr-only`
|
||||
for a11y); that promise lives on **/privacy → “What leaves your device” →
|
||||
Directions**. 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
|
||||
|
|
|
|||
|
|
@ -331,6 +331,21 @@ export function RouteDialog({
|
|||
}, [route]);
|
||||
|
||||
// ── Voice guidance (device TTS — free, offline, no server, no permission) ───
|
||||
// Keep the installed voices fresh; Chrome often fills this list only after
|
||||
// the first voiceschanged event.
|
||||
const voicesRef = useRef<SpeechSynthesisVoice[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || !('speechSynthesis' in window)) return;
|
||||
const synth = window.speechSynthesis;
|
||||
const refresh = () => {
|
||||
voicesRef.current = synth.getVoices();
|
||||
};
|
||||
refresh();
|
||||
synth.addEventListener?.('voiceschanged', refresh);
|
||||
return () => synth.removeEventListener?.('voiceschanged', refresh);
|
||||
}, []);
|
||||
|
||||
const speak = useCallback(
|
||||
(text: string) => {
|
||||
if (!voiceOn) return;
|
||||
|
|
@ -339,24 +354,34 @@ export function RouteDialog({
|
|||
const synth = window.speechSynthesis;
|
||||
const utter = new SpeechSynthesisUtterance(text);
|
||||
utter.rate = 1;
|
||||
const lang = (navigator.language || 'en').slice(0, 2).toLowerCase();
|
||||
const voices = synth.getVoices();
|
||||
// Prefer an on-device voice in the user's language (offline, no network).
|
||||
|
||||
// Follow the APP language, not the phone/OS language. A German user
|
||||
// with an English device must hear German route prompts.
|
||||
const lang = dateLocaleCode();
|
||||
const normalized = lang.toLowerCase();
|
||||
const primary = normalized.split('-')[0];
|
||||
const voices = voicesRef.current.length > 0 ? voicesRef.current : synth.getVoices();
|
||||
const voice =
|
||||
voices.find((v) => v.localService && v.lang.toLowerCase().startsWith(lang)) ??
|
||||
voices.find((v) => v.lang.toLowerCase().startsWith(lang));
|
||||
voices.find((v) => v.localService && v.lang.toLowerCase() === normalized) ??
|
||||
voices.find((v) => v.localService && v.lang.toLowerCase().startsWith(primary)) ??
|
||||
voices.find((v) => v.lang.toLowerCase() === normalized) ??
|
||||
voices.find((v) => v.lang.toLowerCase().startsWith(primary));
|
||||
|
||||
if (voice) {
|
||||
utter.voice = voice;
|
||||
utter.lang = voice.lang;
|
||||
} else {
|
||||
utter.lang = lang;
|
||||
}
|
||||
|
||||
synth.cancel(); // never let prompts queue up and lag behind you
|
||||
synth.speak(utter);
|
||||
} catch {
|
||||
// No TTS — haptics still guide.
|
||||
}
|
||||
},
|
||||
// dateLocaleCode() reads the active app locale at call time, so this
|
||||
// callback stays correct after a language change.
|
||||
[voiceOn],
|
||||
);
|
||||
|
||||
|
|
@ -864,7 +889,7 @@ export function RouteDialog({
|
|||
`.leaflet-container { height: 100% }` override beats a `h-64` on the
|
||||
map element itself, and against a height-less parent that resolved to
|
||||
0 — a fully invisible map with no route line. */}
|
||||
<div className={cn('order-2 relative shrink-0 bg-[#0b0b0c]', following ? 'h-[54vh] min-h-[320px] sm:h-[60vh]' : 'h-[34vh] min-h-[220px] sm:h-[400px]')}>
|
||||
<div className={cn('order-2 relative shrink-0 bg-[#0b0b0c]', following ? 'h-[60vh] min-h-[340px] sm:h-[66vh]' : 'h-[56vh] min-h-[300px] sm:h-[60vh]')}>
|
||||
<div
|
||||
ref={setMapDiv}
|
||||
className="h-full w-full bg-[#0b0b0c]"
|
||||
|
|
|
|||
Loading…
Reference in a new issue