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

25 KiB
Raw Blame History

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-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 (стандартная схема)

Документ готов к согласованию.