15 KiB
BRD: Enduro Trails — Фаза 5 «Редизайн»
Версия: 2.1
Дата: 2026-05-06
Автор: Стрим 🌊
Статус: ✅ Реализовано (с дополнениями)
1. Контекст и проблема
Фазы 3–4 дали полный функционал (роутинг, разведка, связка, красивый маршрут), но UI был dev-прототипом: белый фон, emoji-иконки, панели перекрывали карту, не работало на мобиле в перчатках.
2. Цель
Мобильный UI уровня onX Offroad / Locus Map — тёмный, эндуро-стильный, thumb-friendly. Карта — главный герой. UI — минималистичный HUD.
3. Что реализовано в Фазе 5
✅ Дизайн-система
- Две темы: тёмная (
body.theme-dark) и светлая (body.theme-light) - Авто-режим: SunCalc (CDN) определяет восход/закат по геолокации (fallback: Москва 55.75°N)
- Три режима переключателя: Авто → Светлая → Тёмная → Авто (циклически)
- Сохранение:
localStorage— режим темы сохраняется между сессиями - CSS vars: все компоненты используют
var(--bg),var(--surface),var(--accent)и т.д. - Карта:
map.setStyle()переключает dark/light style.json при смене темы; слои пересоздаются черезmap.on('style.load', onMapStyleLoad)
✅ Layout и компоненты
- Search bar —
position: fixed, top, safe-area, все цвета через CSS vars - Bottom toolbar — 6 режимов (Маршрут, Связка, Красивый, Разведка, Линейка, Метка), SVG Lucide-иконки, активная кнопка = оранжевый фон + белый текст
- Bottom sheets — 4 шита (route/recon/scenic/link), slide-up анимация, drag handle, safe-area padding
- Map controls — компас + геолокация, справа
- Skeleton loading — shimmer-анимация в карточках маршрутов пока идёт запрос
✅ Анимации
- Sheet slide:
transform: translateY+cubic-bezier(0.32, 0, 0.15, 1), 300ms - Кнопки:
scale(0.94)при tap - Карточки маршрутов:
cardFadeInstagger (0/60/120/180/240ms) - Маркеры:
markerPopInscale появление
✅ Свайп вниз для закрытия sheet
initSheetSwipe()— touch-обработка на всех.bottom-sheet- Свайп > 80px →
closeSheet() - Во время свайпа:
translateY(dy)для визуального feedback
✅ Адаптив — десктоп
@media (min-width: 768px)— toolbar вертикально слева, sheet с max-width 400px
✅ Промежуточные точки — drag-and-drop (добавлено в ходе фазы)
- Grip-иконка (6 кружков, Lucide-style) в каждом
wl-item _initWaypointDragHandles(list)— единая логика для touch и mouse- Touch (мобиль): touchstart/touchmove/touchend
- Mouse (десктоп): mousedown → document mousemove/mouseup
- Визуальный feedback:
dragging(opacity 0.4),drag-over-top/drag-over-bottom(border accent) - После drop:
rebuildWaypointMarkers()+renderWaypointsList()+debounceBuildRoute()
✅ Кнопка «Скачать GPX» (изменение в ходе фазы)
- Кнопка «Поделиться» (share dialog с Telegram/WhatsApp) убрана по запросу Славы
- Заменена на прямую кнопку «Скачать GPX» с download-иконкой (стрелка вниз)
onclick="downloadGPX()"— без промежуточных диалогов
✅ Расстояние по маршруту между точками (добавлено 05.05.2026)
-
Функция
getRouteSegmentDistances()— snap каждого waypoint к ближайшей точке геометрии OSRM -
Первая точка →
snapIdx[0] = 0, последняя →snapIdx[n-1]— гарантирует покрытие всей геометрии -
Сумма сегментов масштабируется к
route.distance_m— точное совпадение (diff=0) -
Обновляется при смене варианта (
selectRoute,selectMiniRoute) -
Обновляется после построения маршрута (
drawRouteResults→renderWaypointsList) -
Fallback на haversine по прямой если маршрут ещё не построен
-
Форматирование
toFixed(1)везде (сегменты, карточки вариантов, мини-бар) -
#mini-route-bar— компактная полоска поверх карты когда sheet свёрнут -
Показывает активный маршрут (дистанция, % грунт)
-
Кнопки: добавить точку, развернуть sheet
-
miniAddWaypoint()— теперь корректно устанавливаетrouteMode = trueпередaddingWaypoint = true
✅ Поиск точек маршрута (добавлено 05.05.2026)
- Убран верхний search bar, поиск перенесён прямо в список waypoints
- Кнопка-лупа в каждом
wl-item→ inline Nominatim поиск btn-themeперенесён вmap-controls-r
✅ Метки (named markers, добавлено 05.05.2026)
- 6 типов меток: 🚩 Флаг, 🏕 Лагерь, 🔧 Ремонт, ⛽ Заправка, 💧 Вода, 📍 Точка
- Сохраняются в
localStorage(лимит 50) - Попап с координатами и кнопками: → Точка A, → Точка B, 🗑 Удалить
- Баг исправлен:
removeMarker()теперь явно закрывает попап перед удалением маркера
✅ Линейка — полный UX редизайн (06.05.2026)
Маркеры:
- Первая точка — зелёная (
#2EA043) с подписью «Старт» - Остальные точки — синие с расстоянием сегмента от предыдущей точки
- Крестик × (кнопка
button) справа от расстояния в одной строке — удаляет точку anchor: 'center'— маркер точно привязан к точке тапа- Label абсолютно позиционирован ниже dot
Панель общего расстояния (#ruler-info):
- Компактная:
width: fit-content,max-width: 320px, прижата к левому краю - Показывается только после добавления первой точки
- Кнопка ✓ Завершить — выход из режима рисования, линейка остаётся на карте
- Кнопка ✕ (
deleteRuler()) — удаляет всю линейку
Логика кнопки тулбара (tb-ruler):
- Нет линейки → войти в режим рисования + показать toast-подсказку
- Режим активен → выйти из режима, скрыть линейку (точки сохраняются в памяти)
- Линейка скрыта → показать линейку и войти в режим рисования
Toast-подсказка:
- «Тапни на карту чтобы добавить точку» — появляется при входе в режим, исчезает через 3 сек или при первом тапе
Восстановление панели:
- Тап на линию (
ruler-line) → показать панель + возобновить режим рисования - Тап на маркер линейки → то же самое
updateRulerLabels() — пересчитывает все расстояния и цвета точек с нуля после удаления любой точки
4. Баги исправленные в ходе фазы (включая 06.05.2026)
| Баг | Причина | Фикс |
|---|---|---|
| Кнопка «Добавить точку» не работала | addWaypointMode() не устанавливала routeMode = true |
Добавлена проверка и установка routeMode |
| Web Share API не работал на HTTP | API требует HTTPS | Убран share dialog, оставлено только скачивание GPX |
| Drag-and-drop не работал на десктопе | Только touch-события | Добавлены mouse-события (mousedown/mousemove/mouseup) |
| CSS классы диалога не совпадали с JS | Dev добавил JS с одними классами, CSS с другими | Унифицированы, затем диалог полностью убран |
| Расстояние между точками по прямой | renderWaypointsList вызывался до построения маршрута, routeResults был пуст |
Добавлен renderWaypointsList() после drawRouteResults() |
| Расстояние не обновлялось при смене варианта | selectRoute/selectMiniRoute не вызывали renderWaypointsList |
Добавлен renderWaypointsList() в обе функции |
| Деплой статики не работал | deploy_app2.js копирует только app.py; образ перезаписывает статику при рестарте |
SFTP → docker restart → docker cp (порядок важен!) |
| Крестик × линейки не срабатывал | Слишком маленькая тапабельная зона (span 11px) |
Заменён на button с min-width/height: 32px, font-size: 16px |
| Тап на линию не показывал панель | Общий map.on('click') перехватывал событие раньше |
e.originalEvent.stopPropagation() на ruler-line click |
| Кнопка ✕ в панели не удаляла линейку | toggleRuler() при rulerMode=false + точки есть только показывал панель |
Создана отдельная функция deleteRuler() |
| Попап метки зависал при удалении | removeMarker() не закрывал попап перед удалением |
Добавлен явный popup.remove() перед marker.remove() |
#ruler-info показывался до добавления точек |
Панель открывалась при входе в режим | Панель показывается только после первой точки |
5. Definition of Done — статус
| Критерий | Статус |
|---|---|
| Все P0 тест-кейсы на iPhone SE (375px) | ✅ |
| Bottom sheet плавно открывается/закрывается | ✅ |
| Toolbar: все 6 режимов, min 48px tap target | ✅ |
| Тема: авто (SunCalc) + ручной переключатель | ✅ |
| SVG иконки (Lucide), никаких emoji в UI | ✅ |
| Карта видна при активных панелях | ✅ |
| Safe area корректная | ✅ |
| Десктоп: боковая панель, не ломается layout | ✅ |
| GPX скачивание работает | ✅ |
| Drag-and-drop точек маршрута (touch + mouse) | ✅ |
| Расстояние по маршруту между точками (сумма = route.distance_m) | ✅ |
| Обновление расстояний при смене варианта маршрута | ✅ |
| Деплой + health check OK | ✅ |
| Линейка: расстояние сегмента под каждым маркером | ✅ |
| Линейка: крестик удаления точки с нормальной тапабельной зоной | ✅ |
| Линейка: панель компактная, fit-content | ✅ |
| Линейка: кнопки Завершить / Удалить всё | ✅ |
| Линейка: toast-подсказка при входе в режим | ✅ |
| Линейка: первая точка зелёная «Старт» | ✅ |
| Линейка: тап на линию/маркер возобновляет режим | ✅ |
| Линейка: логика кнопки тулбара (скрыть/показать/рисовать) | ✅ |
| Метки: 6 типов, сохранение в localStorage | ✅ |
| Метки: попап с кнопками A/B/Удалить | ✅ |
| Поиск точек маршрута inline (Nominatim) | ✅ |
6. Технический стек фронтенда
- MapLibre GL JS 4.7.0 (CDN)
- SunCalc 1.9.0 (CDN) — авто-тема по восходу/закату
- Без npm-зависимостей, всё inline CSS/JS
- Деплой: Node.js ssh2 → SFTP на сервер →
docker restart→docker cpпосле рестарта
⚠️ Критически важно: порядок деплоя статики
Образ Docker запекает статику при сборке. При docker restart образ перезаписывает /app/static/. Поэтому:
- SFTP загрузить файлы на сервер (
/home/slin/enduro-trails/prototype/static/) docker restart prototype-enduro-trails-1- Подождать 8 секунд
docker cp /home/slin/.../app.js prototype-enduro-trails-1:/app/static/app.jsdocker cp /home/slin/.../app.css prototype-enduro-trails-1:/app/static/app.cssdocker cp /home/slin/.../index.html prototype-enduro-trails-1:/app/static/index.html
deploy_app2.js — только для app.py (бэкенд).
deploy_static.js — SFTP статики, но cp нужно делать вручную после рестарта.
7. Файлы
| Файл | Описание |
|---|---|
prototype/static/index.html |
HTML, CDN подключения, структура DOM |
prototype/static/app.css |
Все стили, CSS vars, темы, компоненты |
prototype/static/app.js |
Вся логика: карта, роутинг, UI, drag-and-drop |
prototype/app.py |
Бэкенд FastAPI (не трогать) |
Фаза 5 завершена. Следующая: Фаза 6 — SRTM рельеф.