25 KiB
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)
.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 (панели)
.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;
}
Карточка маршрута
.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 (статистика)
.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 (нижняя панель)
<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>
#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 если много):
<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>
Карточка маршрута:
<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 с радиус-контролом. После клика на карте — обновляется.
<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 — Красивый маршрут
<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 Кнопки карты (правый столбец)
<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 (верхняя строка)
<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>
#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-colortransition 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 (стандартная схема)
Документ готов к согласованию.