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

566 lines
25 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.
# BRD: Enduro Trails — Фаза 5 «Редизайн»
**Версия:** 1.0
**Дата:** 2026-05-04
**Автор:** Стрим 🌊
**Статус:** На согласовании
---
## 1. Контекст и проблема
Текущий UI — функциональный прототип, не готовый к реальному использованию. Основные боли:
- **Не мобильный** — панели перекрывают карту, мелкие кнопки, тяжело нажимать в перчатках
- **Нет стиля** — белый фон, emoji-иконки, выглядит как dev-прототип
- **Всё поверх карты** — карта не видна когда работают панели
- **Нет режима вождения** — на мотоцикле невозможно пользоваться
## 2. Цель
Создать мобильный UI уровня onX Offroad / Locus Map — тёмный, эндуро-стильный, thumb-friendly. Карта — главный герой. UI — минималистичный HUD.
## 3. Дизайн-система
### 3.1 Цветовая палитра — две темы
Тема переключается кнопкой ☀️/🌙 в search bar. **Три режима:**
| Режим | Описание |
|-------|----------|
| **Авто** (по умолчанию) | Тема определяется по реальному восходу/закату солнца для текущей геолокации. Если браузер отдал геопозицию — используем её; иначе — запасной город (Москва, 55.75°N). День = светлая, ночь = тёмная. Переключение происходит без перезагрузки. |
| **Светлая** | Принудительно светлая тема, независимо от времени суток. |
| **Тёмная** | Принудительно тёмная тема. |
**Реализация авто-режима:**
- Используется SunCalc (MIT, ~3KB) через CDN: `https://unpkg.com/suncalc@latest`
- `SunCalc.getTimes(date, lat, lng)``sunrise`, `sunset`
- Текущее время между `sunrise` и `sunset` → светлая тема; иначе — тёмная
- При изменении геолокации — пересчёт восхода/заката
- Проверка раз в минуту (на случай, если сессия длительная)
**UI переключателя:** тап на ☀️/🌙 циклически переключает: Авто → Светлая → Тёмная → Авто. Текущий режим показан маленькой подписью под иконкой (или tooltip на десктопе): «Авто», «День», «Ночь». В авто-режиме иконка динамическая: ☀️ если сейчас день, 🌙 если ночь.
**Тёмная (ночная езда):**
```
--bg: #0D1117
--surface: #161B22
--surface2: #21262D
--border: #30363D
--text: #E6EDF3
--text2: #8B949E
--accent: #FF6B00
--gold: #FFD700
--red: #FF3B1F
--success: #2EA043
```
**Светлая (дневная езда):**
```
--bg: #F5F5F0 (бежевый, не слепит на солнце)
--surface: #FFFFFF
--surface2: #F0F0EA
--border: #D0CFC8
--text: #1A1A1A
--text2: #6B6B6B
--accent: #E55A00 (оранжевый чуть темнее — виден на белом)
--gold: #C89B00
--red: #CC2200
--success: #1A7A30
```
Реализация через CSS custom properties на `:root` + класс `body.theme-dark` / `body.theme-light`.
Стиль карты MapLibre меняется соответственно (тёмный/светлый style.json — уже существуют).
### 3.2 Типографика
```
Font: system-ui, -apple-system, 'SF Pro Display', 'Segoe UI', sans-serif
Заголовок: 16px, 700, #E6EDF3
Подзаголовок: 13px, 600, #8B949E, uppercase, letter-spacing: 0.08em
Текст: 14px, 400, #E6EDF3
Мелкий: 12px, 400, #8B949E
Цифры: font-variant-numeric: tabular-nums
```
### 3.3 Иконки
**НЕ emoji!** SVG-иконки через inline SVG или icon font.
Источник: **Lucide Icons** (MIT, 24px stroke, line-cap: round).
Ключевые иконки:
- 🗺 Route: `map` (lucide)
- 📍 Recon: `search` или `radar`
- 🔗 Link: `git-merge`
- 🎨 Scenic: `sparkles`
- 📏 Ruler: `ruler`
- 📌 Marker: `map-pin`
- 🎯 Locate: `navigation`
- 🧭 Compass: `compass`
- ⬇ Download/GPX: `download`
- ✕ Close: `x`
- Add: `plus`
- 🔥 Difficulty: `flame`
- 💧 Water: `droplets`
- 👁 View: `eye`
### 3.4 Компоненты
#### Кнопка карты (FAB style)
```css
.map-btn {
width: 48px; height: 48px;
background: #161B22;
border: 1px solid #30363D;
border-radius: 12px;
color: #E6EDF3;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
transition: all 0.15s;
-webkit-tap-highlight-color: transparent;
}
.map-btn:active { background: #21262D; transform: scale(0.94); }
.map-btn.active { background: #FF6B00; color: #fff; border-color: #FF6B00; }
```
#### Bottom Sheet (панели)
```css
.bottom-sheet {
position: fixed;
bottom: 0; left: 0; right: 0;
background: #161B22;
border-radius: 20px 20px 0 0;
border-top: 1px solid #30363D;
padding: 0 16px 32px; /* 32px = safe area снизу */
z-index: 100;
max-height: 75vh;
overflow-y: auto;
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.32, 0, 0.15, 1);
}
.bottom-sheet.open { transform: translateY(0); }
/* Drag handle */
.sheet-handle {
width: 36px; height: 4px;
background: #30363D;
border-radius: 2px;
margin: 12px auto 16px;
}
```
#### Карточка маршрута
```css
.route-card {
background: #21262D;
border: 1.5px solid #30363D;
border-radius: 12px;
padding: 12px 14px;
margin-bottom: 8px;
cursor: pointer;
transition: border-color 0.15s;
}
.route-card.active {
border-color: #FF6B00;
box-shadow: 0 0 0 1px #FF6B00;
}
```
#### Stat pill (статистика)
```css
.stat-pill {
display: inline-flex; align-items: center; gap: 4px;
background: #0D1117;
border: 1px solid #30363D;
border-radius: 20px;
padding: 3px 10px;
font-size: 12px; font-weight: 600;
color: #E6EDF3;
}
.stat-pill.dirt { border-color: #FFD700; color: #FFD700; }
.stat-pill.asphalt { border-color: #8B949E; color: #8B949E; }
```
## 4. Layout — Mobile First
### Основной экран
```
┌─────────────────────────────────┐
│ [🔍 Поиск...........] [☰] │ ← search bar, 44px, top: safe
│ │
│ │
КАРТА │ ← 100% экрана
│ │
│ [🧭] │
│ [🎯] │ ← кнопки справа, 48×48
│ ────────────────────────────────│
│ [🗺][🔗][🎨][📍][📏][📌] │ ← bottom toolbar, 64px
└─────────────────────────────────┘
```
**Bottom toolbar**: 6 режимов, иконки 24px, tap target 48px minimum.
Активный режим — оранжевый фон + label появляется под иконкой.
### При активном режиме (пример: Маршрут)
```
┌─────────────────────────────────┐
│ [🔍 Поиск...] [☰] │
│ │
КАРТА
│ │
│ [🧭] │
│ [🎯] │
├─────────────────────────────────┤
│ ▬▬▬ drag handle ▬▬▬ │ ← Bottom Sheet
│ 🗺 МАРШРУТ [✕] │
│ ─────────────────────────────── │
│ [A: Хоруговино ] [B: ...] │ ← точки (горизонтально)
│ [+ Точка] [Сбросить] [GPX⬇] │
│ │
│ Строю маршрут... │
│ ┌───────────────────────────┐ │
│ │● Вариант 1 1013 км 14ч │ │
│ │ ████████░░ 97% грунт │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
```
## 5. Компоненты — детальный дизайн
### 5.1 Toolbar (нижняя панель)
```html
<nav id="toolbar">
<button class="tb-btn active" data-mode="route" onclick="toggleRouteMode()">
<svg><!-- map icon --></svg>
<span>Маршрут</span>
</button>
<button class="tb-btn" data-mode="link" onclick="toggleLinkMode()">
<svg><!-- git-merge --></svg>
<span>Связка</span>
</button>
<button class="tb-btn" data-mode="scenic" onclick="toggleScenicMode()">
<svg><!-- sparkles --></svg>
<span>Красивый</span>
</button>
<button class="tb-btn" data-mode="recon" onclick="toggleReconMode()">
<svg><!-- radar --></svg>
<span>Разведка</span>
</button>
<button class="tb-btn" data-mode="ruler" onclick="toggleRuler()">
<svg><!-- ruler --></svg>
<span>Линейка</span>
</button>
<button class="tb-btn" data-mode="marker" onclick="toggleMarkerMode()">
<svg><!-- map-pin --></svg>
<span>Метка</span>
</button>
</nav>
```
```css
#toolbar {
position: fixed;
bottom: 0; left: 0; right: 0;
height: 72px;
background: #161B22;
border-top: 1px solid #30363D;
display: flex;
align-items: center;
justify-content: space-around;
padding: 0 4px;
padding-bottom: env(safe-area-inset-bottom, 0px);
z-index: 200;
}
.tb-btn {
flex: 1;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 3px;
height: 56px;
border: none; background: none;
color: #8B949E;
font-size: 10px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.04em;
border-radius: 10px;
cursor: pointer;
transition: color 0.15s, background 0.15s;
-webkit-tap-highlight-color: transparent;
}
.tb-btn svg { width: 22px; height: 22px; stroke-width: 1.8; }
.tb-btn.active { color: #FF6B00; }
.tb-btn.active svg { stroke: #FF6B00; }
```
### 5.2 Bottom Sheet — Маршрут
Заменяет существующий `#route-panel`.
**Секция точек (горизонтальный scroll если много):**
```html
<div class="waypoints-row">
<div class="wp-chip wp-start">
<div class="wp-dot" style="background:#2EA043"></div>
<span class="wp-label">Хоруговино</span>
</div>
<div class="wp-arrow"></div>
<div class="wp-chip wp-end">
<div class="wp-dot" style="background:#FF3B1F"></div>
<span class="wp-label">Корак-Чурачки</span>
</div>
</div>
```
**Карточка маршрута:**
```html
<div class="route-card active">
<div class="rc-header">
<span class="rc-dot" style="background:#0066ff"></span>
<span class="rc-title">Основной</span>
<span class="rc-km">1013 км</span>
<span class="rc-time">14ч 22м</span>
</div>
<div class="rc-bar">
<div class="rc-bar-dirt" style="width:97%"></div>
<div class="rc-bar-asphalt" style="width:3%"></div>
</div>
<div class="rc-stats">
<span class="stat-pill dirt">🟡 97% грунт</span>
<span class="stat-pill asphalt">⬜ 3% асфальт</span>
</div>
</div>
```
### 5.3 Bottom Sheet — Разведка
При нажатии открывается sheet с радиус-контролом. После клика на карте — обновляется.
```html
<div class="bottom-sheet" id="sheet-recon">
<div class="sheet-handle"></div>
<div class="sheet-header">
<svg><!-- radar icon --></svg>
<h2>Разведка</h2>
<button class="sheet-close" onclick="toggleReconMode()"></button>
</div>
<p class="sheet-hint">Тапни точку на карте — узнаешь сколько грунтовок рядом</p>
<div class="radius-selector">
<button class="radius-btn active" onclick="setReconRadius(20)">20 км</button>
<button class="radius-btn" onclick="setReconRadius(50)">50 км</button>
<button class="radius-btn" onclick="setReconRadius(100)">100 км</button>
</div>
<div id="recon-results" style="display:none">
<div class="recon-section">
<div class="section-label">ГРУНТОВКИ</div>
<div class="recon-grid">
<div class="recon-stat">
<div class="rs-value" id="r-total-km"></div>
<div class="rs-label">км всего</div>
</div>
<div class="recon-stat">
<div class="rs-value rs-gold" id="r-lev12-km"></div>
<div class="rs-label">км Lev1-2</div>
</div>
<div class="recon-stat">
<div class="rs-value rs-red" id="r-lev345-km"></div>
<div class="rs-label">км Lev3-5</div>
</div>
<div class="recon-stat">
<div class="rs-value" id="r-path-km"></div>
<div class="rs-label">км тропы</div>
</div>
</div>
</div>
<div class="recon-section">
<div class="section-label">POI В РАДИУСЕ</div>
<div class="poi-list" id="r-poi-list"></div>
</div>
</div>
</div>
```
### 5.4 Bottom Sheet — Красивый маршрут
```html
<div class="bottom-sheet" id="sheet-scenic">
<div class="sheet-handle"></div>
<div class="sheet-header">
<svg><!-- sparkles --></svg>
<h2>Красивый маршрут</h2>
<button class="sheet-close" onclick="toggleScenicMode()"></button>
</div>
<div id="scenic-start-prompt">
<p class="sheet-hint">Тапни точку старта на карте</p>
</div>
<div id="scenic-config" style="display:none">
<div class="section-label">ДИСТАНЦИЯ</div>
<div class="dist-row">
<button class="dist-btn" data-km="50">50</button>
<button class="dist-btn active" data-km="100">100</button>
<button class="dist-btn" data-km="150">150</button>
<button class="dist-btn" data-km="200">200</button>
<input type="number" id="dist-custom" placeholder="км" min="20" max="500">
</div>
<button class="btn-primary" onclick="buildScenicRoute()">
<svg><!-- sparkles --></svg>
Построить маршрут
</button>
</div>
<div id="scenic-cards"></div>
</div>
```
### 5.5 Кнопки карты (правый столбец)
```html
<div id="map-controls-r">
<button class="map-btn" title="Компас" id="btn-compass" onclick="toggleCompass()">
<svg><!-- compass --></svg>
</button>
<button class="map-btn" title="Моё местоположение" onclick="locateMe()">
<svg><!-- navigation --></svg>
</button>
</div>
```
### 5.6 Search Bar (верхняя строка)
```html
<div id="search-bar">
<svg><!-- search icon --></svg>
<input type="text" id="search-input" placeholder="Поиск места..." autocomplete="off">
<button id="btn-menu" onclick="toggleMenu()">
<svg><!-- menu --></svg>
</button>
</div>
```
```css
#search-bar {
position: fixed;
top: env(safe-area-inset-top, 12px);
left: 12px; right: 12px;
height: 48px;
background: #161B22;
border: 1px solid #30363D;
border-radius: 14px;
display: flex; align-items: center;
padding: 0 14px;
gap: 10px;
z-index: 200;
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
}
#search-input {
flex: 1;
background: none; border: none;
color: #E6EDF3; font-size: 15px;
outline: none;
}
#search-input::placeholder { color: #484F58; }
```
## 6. Анимации и микроинтерактивность
- **Bottom sheet**: `transform: translateY` + `cubic-bezier(0.32, 0, 0.15, 1)`, 280ms
- **Кнопки**: `transform: scale(0.94)` при tap (active state), 100ms
- **Карточки маршрутов**: `border-color` transition 150ms
- **Loading**: пульсирующий skeleton (opacity animation) вместо spinner
- **Маркеры на карте**: появление через `transform: scale(0) → scale(1)`, 200ms
## 7. Адаптив
- **Мобила (< 768px)**: bottom sheet, toolbar снизу
- **Десктоп (≥ 768px)**: боковая панель 320px слева, кнопки сохраняются
- **Landscape мобила**: toolbar справа вертикально, sheet с max-height: 80vh
## 8. Тест-кейсы
### P0 — Критично (должно работать идеально)
| ID | Сценарий | Устройство | Шаги | Ожидаемый результат |
|----|----------|-----------|------|---------------------|
| T01 | Открытие приложения | iPhone SE (375px) | Открыть URL | Карта 100% экрана, toolbar снизу виден, search bar сверху |
| T02 | Нажать режим Маршрут | iPhone | Тап на иконку 🗺 | Bottom sheet выезжает снизу, иконка оранжевая |
| T03 | Установить A и B точки | iPhone | Тап A, тап B | Маркеры на карте, карта НЕ перекрыта |
| T04 | Маршрут построен | iPhone | После T03 | Карточки маршрутов в sheet, карта видна |
| T05 | Тап по карточке | iPhone | Тап на вариант 2 | Маршрут 2 активен (оранжевый), карточка выделена |
| T06 | Закрыть sheet | iPhone | Тап ✕ или свайп вниз | Sheet уходит, toolbar возвращается, карта чистая |
| T07 | Разведка — тап на карту | iPhone | Включить Разведку, тапнуть | Круг на карте, sheet со статистикой |
| T08 | Переключение радиуса | iPhone | В sheet Разведки нажать 50км | Круг обновился, статистика пересчиталась |
| T09 | Красивый маршрут | iPhone | Включить, тапнуть, нажать «Построить» | Кольцевой маршрут на карте, карточки в sheet |
| T10 | Связка | iPhone | Включить, тапнуть 2 точки | Маршрут между точками, карточки |
| T11 | Поиск места | iPhone | Тапнуть search bar, ввести «Тверь» | Результаты поиска, тап → карта летит |
| T12 | GPX скачать | iPhone | Построить маршрут → Download | Файл скачался |
### P1 — Важно
| ID | Сценарий | Устройство | Шаги | Ожидаемый результат |
|----|----------|-----------|------|---------------------|
| T13 | Тема — авто по умолчанию | Любое | Открыть (днём) | Фон светлый (бежевый #F5F5F0), иконка ☀️, подпись «Авто» |
| T13a | Тема — авто ночью | Любое | Открыть (после заката) | Фон тёмный #0D1117, иконка 🌙, подпись «Авто» |
| T13b | Тема — ручной цикл | Любое | Тап ☀️/🌙 3 раза | Авто → Светлая → Тёмная → Авто, при каждом тапе тема и подпись меняются |
| T13c | Тема — ручная не зависит от времени | Любое | Переключить на «Светлая» ночью | Фон бежевый, подпись «День», иконка ☀️ |
| T13d | Тема — авто пересчитывает при геолокации | Любое | В авто-режиме, разрешить геолокацию | Восход/закат пересчитаны по реальным координатам, тема соответствует |
| T14 | Толстые пальцы в перчатках | iPhone | Нажимать кнопки | Все кнопки min 48×48px, не промахиваешься |
| T15 | Landscape поворот | iPhone | Повернуть телефон | UI перестроился, карта видна |
| T16 | Десктоп Chrome | MacBook | Открыть | Боковая панель 320px, кнопки слева |
| T17 | Свайп вниз для закрытия | iPhone | Свайп вниз по handle | Sheet закрывается |
| T18 | Два режима не активны одновременно | iPhone | Открыть Маршрут, потом Разведку | Маршрут закрылся, Разведка открылась |
| T19 | Геолокация | iPhone | Нажать 🎯 | Запрос разрешения, потом маркер на карте |
| T20 | Компас | iPhone | Нажать 🧭 | Карта вращается, кнопка активна |
| T21 | Метка — добавить | iPhone | Включить 📌, тапнуть | Диалог выбора типа метки (popup) |
| T22 | Метка — иконка на карте | iPhone | После T21 | Метка видна, тап открывает popup с опциями |
| T23 | Линейка | iPhone | Включить 📏, тапнуть несколько точек | Линия + дистанция |
| T24 | Анимация sheet | iPhone | Открыть/закрыть | Плавная, 280ms, без дёрганий |
| T25 | Skeleton loading | iPhone | Построить длинный маршрут | Skeleton в карточках пока грузится |
| T26 | Ошибка маршрута | iPhone | Поставить точки в море | Понятное сообщение об ошибке в sheet |
### P2 — Nice to have
| ID | Сценарий | Шаги | Ожидаемый результат |
|----|----------|------|---------------------|
| T27 | Safari iOS | Открыть | Safe area работает, нет обрезания снизу |
| T28 | Медленный интернет | Throttle 3G | Skeleton, потом данные |
| T29 | Очень длинное название места | Поставить точку у «деревня Нижние Бородавки» | Название обрезается с ellipsis |
| T30 | 3 альтернативных маршрута | Построить маршрут | 3 карточки, можно переключать |
| T31 | Scenic score визуализация | В карточке Красивого | Звёздочки или bar для scenic_score |
| T32 | POI на маршруте в Красивом | В карточке | Иконки POI с названиями |
| T33 | Кнопки режима вместе видны | На экране 375px | Все 6 иконок в toolbar без обрезания |
| T34 | Статус бар iOS | iPhone | Нет перекрытия search bar статус-баром |
## 9. Definition of Done
- [ ] Все P0 тест-кейсы прошли на iPhone SE (375px)
- [ ] Все P0 + P1 тест-кейсы прошли на iPhone 14 Pro (393px)
- [ ] Bottom sheet плавно открывается/закрывается
- [ ] Toolbar: все 6 режимов, min 48px tap target
- [ ] Тема: авто (по восходу/закату SunCalc) + ручной переключатель (3 режима: Авто/Светлая/Тёмная)
- [ ] Тёмная тема везде, нет белых вспышек; светлая тема без ослепляющего белого
- [ ] SVG иконки (Lucide), никаких emoji в UI-элементах
- [ ] Карта видна при активных панелях
- [ ] Safe area корректная (notch, home indicator)
- [ ] Десктоп: боковая панель, не ломается layout
- [ ] GPX скачивание работает
- [ ] Деплой + health check OK
## 10. Технические ограничения
- Менять бэкенд (app.py) нельзя — только фронт
- MapLibre GL остаётся
- Без новых npm-зависимостей (только inline CSS/JS)
- Lucide иконки — подключить через CDN: `https://unpkg.com/lucide@latest`
- SunCalc — подключить через CDN: `https://unpkg.com/suncalc@latest` (MIT, ~3KB, для расчёта восхода/заката)
- Деплой через ssh2 (стандартная схема)
---
*Документ готов к согласованию.*