213 lines
15 KiB
Markdown
213 lines
15 KiB
Markdown
# 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
|
||
- Карточки маршрутов: `cardFadeIn` stagger (0/60/120/180/240ms)
|
||
- Маркеры: `markerPopIn` scale появление
|
||
|
||
### ✅ Свайп вниз для закрытия 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/`. Поэтому:
|
||
|
||
1. SFTP загрузить файлы на сервер (`/home/slin/enduro-trails/prototype/static/`)
|
||
2. `docker restart prototype-enduro-trails-1`
|
||
3. Подождать 8 секунд
|
||
4. `docker cp /home/slin/.../app.js prototype-enduro-trails-1:/app/static/app.js`
|
||
5. `docker cp /home/slin/.../app.css prototype-enduro-trails-1:/app/static/app.css`
|
||
6. `docker 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 рельеф.*
|