Files
wiki/tasks/enduro-trails/DEV_TASK_PHASE5_UX2.md
2026-05-05 22:10:01 +03:00

224 lines
8.9 KiB
Markdown
Raw 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.
# Dev Task: Enduro Trails — UX маршрута + индикатор загрузки
**Приоритет:** HIGH
**Проект:** enduro-trails
**Дата:** 2026-05-05
---
## Контекст
Текущий UX маршрута неудобен:
1. После выбора двух точек автоматически открывается основной sheet — карта скрывается
2. «Добавить точку» в основном sheet не скрывает sheet — карта не видна для тапа
3. Основной sheet открывается сразу, хотя достаточно мини-бара
4. Нет индикатора что маршрут строится — пользователь не понимает что происходит
**Файлы:** ТОЛЬКО `app.js`, `app.css`, `index.html` (бэкенд НЕ трогать)
---
## Задача 1: Не открывать основной sheet автоматически
### Текущее поведение
В `initRouteClicks` при добавлении второй точки вызывается `buildRoute()` — который внутри ничего не открывает, НО `toggleRouteMode()` при входе в режим уже открыл sheet через `openSheet('sheet-route')`.
### Нужное поведение
- Вход в режим маршрута (`toggleRouteMode()`) → **НЕ открывать** `sheet-route` автоматически
- Sheet открывается ТОЛЬКО по явному тапу на мини-бар (уже работает через `showMiniRouteSheet`)
- Маршрут строится в фоне, карта остаётся видна, мини-бар показывает прогресс
### Изменения в `toggleRouteMode()`:
```js
function toggleRouteMode() {
const btn = document.getElementById('tb-route');
if (routeMode) {
// Если sheet открыт — закрыть sheet (но не выходить из режима)
const sheet = document.getElementById('sheet-route');
if (sheet && sheet.classList.contains('open')) {
closeSheet('sheet-route');
return;
}
// Если sheet закрыт — выйти из режима и сбросить маршрут
routeMode = false;
btn.classList.remove('active');
clearRoute();
window._map.getCanvas().style.cursor = '';
} else {
// Войти в режим маршрута — НЕ открывать sheet
deactivateAllModes();
routeMode = true;
btn.classList.add('active');
clearRoute();
window._map.getCanvas().style.cursor = 'crosshair';
// sheet НЕ открываем — пользователь сам тапнет на мини-бар
}
updateMapModeClass();
}
```
---
## Задача 2: «Добавить точку» скрывает sheet
### В `addWaypointMode()`:
```js
function addWaypointMode() {
if (routeWaypoints.length >= 10) return;
if (!routeMode) {
routeMode = true;
document.getElementById('tb-route').classList.add('active');
updateMapModeClass();
}
addingWaypoint = true;
window._map.getCanvas().style.cursor = 'crosshair';
// Скрыть основной sheet чтобы карта была видна
closeSheet('sheet-route');
// Показать подсказку в мини-баре
const statsEl = document.getElementById('mini-stats');
if (statsEl) statsEl.textContent = 'Тапни на карте для добавления точки';
document.getElementById('sheet-route-mini').classList.add('visible');
}
```
---
## Задача 3: После построения маршрута — только мини-бар
### В `buildRoute()` — убрать автоматическое открытие sheet, показывать только мини-бар:
```js
async function buildRoute() {
if (routeWaypoints.length < 2) return;
// Показать мини-бар с индикатором загрузки
showMiniRouteLoading();
// Закрыть основной sheet если открыт
closeSheet('sheet-route');
document.getElementById('route-status').textContent = 'Строю маршрут...';
try {
const resp = await fetch(...);
// ... существующая логика ...
drawRouteResults(routeResults, 0);
// После успеха — показать мини-бар с результатом (НЕ открывать sheet)
showMiniRouteSheet();
hideMiniRouteLoading();
document.getElementById('route-status').textContent = `${routeResults.length} маршрут(ов)`;
} catch(e) {
hideMiniRouteLoading();
document.getElementById('route-status').textContent = '❌ ' + e.message;
// Показать ошибку в мини-баре
const statsEl = document.getElementById('mini-stats');
if (statsEl) statsEl.textContent = '❌ ' + e.message;
}
}
```
---
## Задача 4: Индикатор загрузки — колесо мотоцикла
### Анимация
В мини-баре (`#sheet-route-mini`) показывать SVG колесо мотоцикла которое крутится пока строится маршрут.
**SVG колесо (кросс/эндуро стиль — спицы, грунтозацепы):**
```svg
<svg class="moto-wheel" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<!-- Обод -->
<circle cx="20" cy="20" r="17" fill="none" stroke="var(--accent)" stroke-width="2.5"/>
<!-- Шина (внешний контур с грунтозацепами — неровный) -->
<circle cx="20" cy="20" r="19" fill="none" stroke="var(--text2)" stroke-width="1.5" stroke-dasharray="3 2"/>
<!-- Ступица -->
<circle cx="20" cy="20" r="3" fill="var(--accent)"/>
<!-- Спицы (8 штук) -->
<line x1="20" y1="3" x2="20" y2="17" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
<line x1="20" y1="23" x2="20" y2="37" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
<line x1="3" y1="20" x2="17" y2="20" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
<line x1="23" y1="20" x2="37" y2="20" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
<line x1="7.9" y1="7.9" x2="17.5" y2="17.5" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
<line x1="22.5" y1="22.5" x2="32.1" y2="32.1" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
<line x1="32.1" y1="7.9" x2="22.5" y2="17.5" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
<line x1="17.5" y1="22.5" x2="7.9" y2="32.1" stroke="var(--text2)" stroke-width="1.2" stroke-linecap="round"/>
</svg>
```
**CSS анимация:**
```css
.moto-wheel {
width: 32px; height: 32px;
animation: wheelSpin 0.8s linear infinite;
display: none;
flex-shrink: 0;
}
.moto-wheel.spinning {
display: block;
}
@keyframes wheelSpin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
```
### Где показывать
В `#sheet-route-mini` добавить элемент колеса рядом с `#mini-stats`:
```html
<!-- В index.html, внутри #sheet-route-mini -->
<svg id="mini-wheel" class="moto-wheel" viewBox="0 0 40 40" ...>...</svg>
```
**Функции управления:**
```js
function showMiniRouteLoading() {
const wheel = document.getElementById('mini-wheel');
const statsEl = document.getElementById('mini-stats');
if (wheel) wheel.classList.add('spinning');
if (statsEl) statsEl.textContent = 'Строю маршрут...';
document.getElementById('sheet-route-mini').classList.add('visible');
}
function hideMiniRouteLoading() {
const wheel = document.getElementById('mini-wheel');
if (wheel) wheel.classList.remove('spinning');
}
```
---
## Итоговый UX-флоу
```
Тап на 🗺 → routeMode=true, cursor=crosshair, sheet НЕ открывается
Тап на карте (точка A) → маркер A, cursor=crosshair
Тап на карте (точка B) → маркер B, мини-бар появляется с крутящимся колесом
Маршрут построен → колесо исчезает, мини-бар показывает "120 км · 87% грунт"
Тап на мини-бар → открывается основной sheet с деталями
Тап «Добавить точку» в sheet → sheet скрывается, карта видна, cursor=crosshair
Тап на карте → точка добавлена, маршрут перестраивается (колесо в мини-баре)
```
---
## Деплой
```
/tmp/deploy_static.js
SSH: host=82.22.50.71, port=22, user=slin, pass=motoZ@yaz2010
Health: curl -s http://mva154:5558/api/health
```
## Ограничения
- НЕ трогать `app.py`
- Без npm-зависимостей
- Сохранить всю существующую логику (drag-and-drop, geocoding, GPX, etc.)