diff --git a/tasks/enduro-trails/BRD_WAYPOINT_SEARCH.md b/tasks/enduro-trails/BRD_WAYPOINT_SEARCH.md index a9ca11f..598eeff 100644 --- a/tasks/enduro-trails/BRD_WAYPOINT_SEARCH.md +++ b/tasks/enduro-trails/BRD_WAYPOINT_SEARCH.md @@ -1,6 +1,6 @@ # BRD: Enduro Trails — Поиск точек маршрута -**Версия:** 1.0 +**Версия:** 1.1 **Дата:** 2026-05-05 **Автор:** Стрим 🌊 **Статус:** 🔄 В разработке @@ -17,7 +17,7 @@ ## 2. Цель -Убрать верхний search bar. Добавить поиск прямо в список точек маршрута — inline, для каждой точки отдельно. +Убрать верхний search bar. Добавить поиск прямо в список точек маршрута — inline, для каждой точки отдельно. При первом открытии режима маршрута — онбординг с полями ввода старта и финиша. --- @@ -30,9 +30,36 @@ - Удалить вызов `initSearch()` из JS - Кнопку темы (`#btn-theme`) перенести в `#map-controls-r` (рядом с компасом и геолокацией) -**Почему:** bar занимал ~52px сверху, перекрывал карту, дублировал функционал который теперь встроен в waypoints. +### 3.2 Онбординг при активации режима маршрута -### 3.2 Inline поиск в каждой точке маршрута +Когда пользователь тапает «Маршрут» в toolbar и точек ещё нет — показывать вместо пустого списка: + +``` +┌─────────────────────────────────┐ +│ 🔍 Откуда? │ ← поле поиска старта +│ или тапни на карте │ ← подсказка +└─────────────────────────────────┘ +``` + +После выбора старта автоматически появляется: +``` +┌─────────────────────────────────┐ +│ S Название старта │ ← добавленная точка +├─────────────────────────────────┤ +│ 🔍 Куда? │ ← поле поиска финиша +│ или тапни на карте │ +└─────────────────────────────────┘ +``` + +После выбора финиша — поля исчезают, маршрут строится, показывается обычный `wl-list`. + +**Детали:** +- Поля поиска используют тот же Nominatim что и inline поиск в wl-item +- Подсказка «или тапни на карте» — мелкий текст `var(--text3)` под полем +- Тап на карте тоже работает — добавляет точку и переходит к следующему полю +- Если точки уже есть — онбординг не показывается, сразу wl-list + +### 3.3 Inline поиск в каждой точке маршрута В каждом `wl-item` добавить кнопку-лупу. Тап → разворачивается inline панель поиска прямо под точкой. @@ -77,7 +104,7 @@ | Аспект | Решение | |--------|---------| | Поиск API | Nominatim `/search` (уже используется в проекте) | -| Debounce | 400ms (как в старом search bar) | +| Debounce | 400ms | | Состояние | `wpSearchTimeout` — глобальная переменная для debounce | | Закрытие панели | Тап на лупу повторно — закрывает; открытие другой — закрывает текущую | | После выбора | `routeWaypoints[idx]` = `{lat, lon}`, rebuild + route | @@ -85,12 +112,29 @@ --- -## 6. Definition of Done +## 6. Баги после редизайна (требуют исправления) + +| Баг | Описание | Вероятная причина | +|-----|---------|------------------| +| Метки в верхнем левом углу | Попап метки отображается не на позиции маркера, а в углу экрана | CSS `position` маркера сбивает позиционирование MapLibre | +| Метки линейки в верхнем углу | Попап точки линейки отображается не на треке, а в углу | Та же причина — CSS позиционирование маркера | +| Подсказка «Тапни на карте» пропала | `#route-status` не виден когда точек нет | Скрыт или перекрыт после редизайна | + +**Вероятная причина багов с маркерами:** в редизайне изменились глобальные CSS стили для `position`, `transform` или `z-index` — это сбивает позиционирование MapLibre маркеров. Маркер MapLibre должен иметь `position: absolute` без переопределения в глобальных стилях. + +**Фикс:** проверить CSS для `.maplibregl-marker`, `.marker-flag`, `.ruler-marker` — убрать любой `position: fixed/relative/static` который может переопределять поведение маркера. + +--- + +## 7. Definition of Done | Критерий | Статус | |----------|--------| | Верхний search bar отсутствует | ⬜ | | Кнопка темы работает в map-controls-r | ⬜ | +| При активации маршрута без точек — поле поиска старта | ⬜ | +| После старта — поле поиска финиша | ⬜ | +| Подсказка «или тапни на карте» видна | ⬜ | | Иконка лупы в каждом wl-item | ⬜ | | Тап на лупу → inline поиск открывается | ⬜ | | Поиск находит места через Nominatim | ⬜ | @@ -98,37 +142,56 @@ | Маршрут перестраивается после выбора | ⬜ | | Карта летит к выбранному месту | ⬜ | | Одновременно открыта только одна панель | ⬜ | +| Метки отображаются на позиции маркера (не в углу) | ⬜ | +| Метки линейки отображаются на треке | ⬜ | | Деплой + health check OK | ⬜ | --- -## 7. Файлы +## 8. Файлы | Файл | Изменения | |------|-----------| | `prototype/static/index.html` | Удалить `#search-bar`, `#search-results`; перенести `#btn-theme` в `#map-controls-r` | -| `prototype/static/app.css` | Удалить стили search bar; добавить `.wl-search-btn`, `.wl-search-panel`, `.wl-search-input`, `.wl-search-results`, `.wl-search-result-item` | -| `prototype/static/app.js` | Удалить `initSearch()`; добавить `openWaypointSearch()`, `doWaypointSearch()`, `selectWaypointSearchResult()` | +| `prototype/static/app.css` | Удалить стили search bar; добавить `.wl-search-*`; исправить позиционирование маркеров | +| `prototype/static/app.js` | Удалить `initSearch()`; добавить онбординг, `openWaypointSearch()`, `doWaypointSearch()`, `selectWaypointSearchResult()` | --- -## 8. Дополнительные требования (05.05.2026) +--- -### 8.1 Онбординг при активации режима маршрута +## 9. Дополнительные замечания (05.05.2026 — пакет 2) -Когда пользователь тапает кнопку «Маршрут» в toolbar и точек ещё нет — показывать: +| # | Замечание | Детали | +|---|-----------|--------| +| 1 | Иконка колеса | Заменить на нормальное мотокросс колесо | +| 2 | Флажок финиша | Чёрно-белая шахматная раскраска как финишный флаг | +| 3 | Спиннер в основном листе | Вращающееся колесо пока строится маршрут (как в мини-баре) | +| 4 | Тёмная карта | При тёмной теме — тёмный стиль карты, при светлой — светлый | +| 5 | Автозум на маршрут | Плавный `fitBounds` после построения маршрута, маршрут по центру без выхода за границы | +| 6 | Мини-бар перекрывает кнопки справа | Исправить z-index/позиционирование `#sheet-route-mini` | -1. **Поле ввода/поиска для старта** с placeholder «Откуда? Введи название или тапни на карте» -2. После выбора старта — **поле ввода/поиска для финиша** с placeholder «Куда? Введи название или тапни на карте» -3. Подсказка под полем: «или тапни на карте» (мелкий текст, `var(--text3)`) +### Детали по пунктам -**UX flow:** -- Открылся sheet маршрута, точек нет → показать поле старта -- Пользователь вводит → Nominatim поиск → выбирает → точка S добавлена -- Автоматически появляется поле финиша -- Пользователь вводит или тапает на карте → точка F добавлена → маршрут строится -- После добавления обеих точек — поля исчезают, показывается обычный `wl-list` +**П.1 Иконка колеса:** +Заменить текущую SVG на нормальное мотокросс колесо — спицы, кноблинг, центральная втулка. Использовать Lucide `bike` или нарисовать SVG колеса с спицами. -### 8.2 Подсказка «Тапни на карте» +**П.2 Флажок финиша:** +В `waypointPinSvg('F', ...)` для финишной точки использовать шахматный узор (SVG `pattern` чёрные/белые клетки) вместо красного цвета. -`#route-status` с текстом «Тапни точку старта на карте» пропал после редизайна — восстановить видимость. Должен показываться когда точек нет или когда активен режим добавления точки (`addingWaypoint = true`). +**П.3 Спиннер в основном листе:** +В `#route-cards` показывать skeleton/спиннер пока идёт запрос к OSRM. Тот же спиннер что в мини-баре. + +**П.4 Тёмная карта:** +`map.setStyle()` при смене темы. Тёмный стиль: `/style-dark.json` (или текущий тёмный), светлый: `/style-light.json` (или текущий светлый). После `setStyle` пересоздать слои маршрутов через `map.on('style.load', onMapStyleLoad)`. + +**П.5 Автозум:** +После `drawRouteResults()` вызывать: +```js +const coords = routeResults[0].geometry.coordinates; +const bounds = coords.reduce((b, c) => b.extend(c), new maplibregl.LngLatBounds(coords[0], coords[0])); +map.fitBounds(bounds, { padding: 60, duration: 1000, maxZoom: 14 }); +``` + +**П.6 Мини-бар:** +`#sheet-route-mini` перекрывает `#map-controls-r`. Исправить `right` или `width` мини-бара чтобы не залезал на кнопки справа. diff --git a/tasks/enduro-trails/prototype/static/app.css b/tasks/enduro-trails/prototype/static/app.css index b6582e6..dcbc7bf 100644 --- a/tasks/enduro-trails/prototype/static/app.css +++ b/tasks/enduro-trails/prototype/static/app.css @@ -305,7 +305,7 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; } .wl-add .wl-label { color: var(--text3); } /* ── Route Status ─────────────────────────────── */ -#route-status { font-size: 13px; color: var(--text2); padding: 8px 0; display: flex; align-items: center; gap: 6px; } +#route-status { font-size: 13px; color: var(--text2); padding: 8px 0; display: flex; align-items: center; gap: 6px; min-height: 20px; } /* ── Route Cards ──────────────────────────────── */ #route-cards, #link-cards, #scenic-cards { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; } @@ -451,6 +451,11 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; } } #ruler-info.visible { display: flex; align-items: center; gap: 8px; } +/* ── Fix: MapLibre markers must stay absolute ────── */ +.maplibregl-marker { + position: absolute !important; +} + /* ── Waypoint Markers ─────────────────────────── */ .route-waypoint-marker { filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4)); width: 28px; height: 36px; cursor: grab; display: block; } .route-waypoint-marker:active { cursor: grabbing; } @@ -509,6 +514,35 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; } } .marker-anim { animation: markerPopIn 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28) both; } +/* ── Onboarding (empty waypoints state) ─────────── */ +.wl-onboarding { + padding: 4px 0; +} +.wl-onboard-field { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 8px 12px 8px 0; +} +.wl-onboard-input { + flex: 1; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + font-size: 14px; + padding: 8px 12px; + outline: none; + box-sizing: border-box; +} +.wl-onboard-input:focus { border-color: var(--accent); } +.wl-onboard-hint { + text-align: center; + font-size: 12px; + color: var(--text3); + padding: 4px 0 8px; +} + /* ── Misc ────────────────────────────────────── */ .text-accent { color: var(--accent); } .text-gold { color: var(--gold); } @@ -595,7 +629,7 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; } /* ── Mini Route Bar ───────────────────────── */ #sheet-route-mini { position: fixed; - bottom: 72px; left: 0; right: 0; + bottom: 72px; left: 0; right: 56px; height: 64px; background: var(--surface); border-top: 1px solid var(--border); @@ -646,6 +680,23 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; } #sheet-route-mini { left: 72px; width: 380px; right: auto; border-radius: 0 14px 0 0; } } +/* ── Route Loading Spinner ───────────────────── */ +.route-loading { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 32px 16px; +} +.route-spinner { + width: 32px; + height: 32px; + border: 3px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + /* ── Moto Wheel Loading Indicator ────────────── */ .moto-wheel { width: 32px; height: 32px; diff --git a/tasks/enduro-trails/prototype/static/app.js b/tasks/enduro-trails/prototype/static/app.js index aacd326..95e0180 100644 --- a/tasks/enduro-trails/prototype/static/app.js +++ b/tasks/enduro-trails/prototype/static/app.js @@ -528,6 +528,27 @@ async function reverseGeocode(lat, lon) { function waypointPinSvg(label, color) { const fs = label.length > 1 ? '7' : '9'; + + // Finish flag — checkered pattern + if (label === 'F') { + const uid = Math.random().toString(36).slice(2); + return ` + + + + + + + + + + + + + ${label} + `; + } + return ` ${label} @@ -605,7 +626,29 @@ function getRouteSegmentDistances() { async function renderWaypointsList() { const list = document.getElementById('waypoints-list'); - if (!routeWaypoints.length) { list.innerHTML = ''; return; } + + // ── Onboarding: no waypoints yet ────────────────────────────── + if (!routeWaypoints.length) { + list.innerHTML = ` + + + ${waypointPinSvg('S', '#2EA043')} + + + + + + или тапни на карте + `; + _initOnboardSearch('start'); + _initWaypointDragHandles(list); + return; + } + + // ── Onboarding: only start added, need finish ────────────────── + // (handled below after normal list render) const gripSvg = ``; @@ -649,8 +692,28 @@ async function renderWaypointsList() { `; } + // Onboarding finish field: only start added, no finish yet + if (routeWaypoints.length === 1) { + html += ` + + ${waypointPinSvg('F', '#FF3B1F')} + + + + + + или тапни на карте`; + } + list.innerHTML = html; + // Init finish onboard search if only 1 waypoint + if (routeWaypoints.length === 1) { + _initOnboardSearch('finish'); + } + // Async geocode routeWaypoints.forEach(async (wp, i) => { const name = await reverseGeocode(wp.lat, wp.lon); @@ -662,6 +725,64 @@ async function renderWaypointsList() { _initWaypointDragHandles(list); } +// ─── Onboard search helpers ──────────────────────────────────────── +function _initOnboardSearch(type) { + const input = document.getElementById(`wl-onboard-input-${type}`); + const resultsEl = document.getElementById(`wl-onboard-results-${type}`); + if (!input) return; + + let timeout = null; + input.addEventListener('input', () => { + clearTimeout(timeout); + const q = input.value.trim(); + if (q.length < 2) { resultsEl.innerHTML = ''; return; } + timeout = setTimeout(() => _doOnboardSearch(type, q, resultsEl), 400); + }); + + // Autofocus only for start field (finish field appears inline) + if (type === 'start') { + setTimeout(() => input.focus(), 100); + } +} + +async function _doOnboardSearch(type, query, resultsEl) { + resultsEl.innerHTML = 'Поиск...'; + try { + const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&limit=5&countrycodes=ru&accept-language=ru`; + const resp = await fetch(url); + const data = await resp.json(); + if (!data.length) { + resultsEl.innerHTML = 'Ничего не найдено'; + return; + } + resultsEl.innerHTML = data.map(item => { + const parts = (item.display_name || '').split(', '); + const name = parts[0]; + const sub = parts.slice(1, 3).join(', '); + return ` + ${name} + ${sub ? `${sub}` : ''} + `; + }).join(''); + } catch(e) { + resultsEl.innerHTML = 'Ошибка'; + } +} + +function _selectOnboardResult(type, lat, lon, name) { + const wp = { lat: parseFloat(lat), lon: parseFloat(lon) }; + if (type === 'start') { + routeWaypoints.unshift(wp); + } else { + routeWaypoints.push(wp); + } + rebuildWaypointMarkers(); + renderWaypointsList(); + window._map.flyTo({ center: [parseFloat(lon), parseFloat(lat)], zoom: 12, duration: 600 }); + if (routeWaypoints.length >= 2) debounceBuildRoute(); + updateMiniRouteCard(); +} + function _initWaypointDragHandles(list) { let dragIdx = -1; let startY = 0; @@ -817,6 +938,7 @@ async function buildRoute() { // Show mini-bar with spinning wheel showMiniRouteLoading(); + showRouteLoading(); // Close main sheet if open closeSheet('sheet-route'); @@ -905,6 +1027,23 @@ function drawRouteResults(routes, activeIdx) { renderRouteCards(routes); + // Auto-zoom to active route after drawing + const activeRoute = routes[activeIdx] || routes[0]; + if (activeRoute && activeRoute.geometry && activeRoute.geometry.coordinates) { + const coords = activeRoute.geometry.coordinates; + if (coords.length > 1) { + const bounds = coords.reduce( + (b, c) => b.extend(c), + new maplibregl.LngLatBounds(coords[0], coords[0]) + ); + map.fitBounds(bounds, { + padding: { top: 80, bottom: 160, left: 20, right: 20 }, + duration: 1200, + maxZoom: 14 + }); + } + } + // Update mini sheet if visible const miniEl = document.getElementById('sheet-route-mini'); if (miniEl && miniEl.classList.contains('visible')) showMiniRouteSheet(); @@ -2022,7 +2161,15 @@ function selectMiniRoute(idx) { renderWaypointsList(); } -// ─── Mini Route Loading Indicator ───────────────────────────────── +// ─── Route Loading Indicators ──────────────────────────────────── +function showRouteLoading() { + const el = document.getElementById('route-cards'); + if (el) el.innerHTML = ` + + Строю маршрут... + `; +} + function showMiniRouteLoading() { const wheel = document.getElementById('mini-wheel'); const statsEl = document.getElementById('mini-stats'); diff --git a/tasks/enduro-trails/prototype/static/index.html b/tasks/enduro-trails/prototype/static/index.html index 0ffcbf4..4693507 100644 --- a/tasks/enduro-trails/prototype/static/index.html +++ b/tasks/enduro-trails/prototype/static/index.html @@ -228,22 +228,20 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + +