From 15b4aace79e5b8452572dfe8714d4cbeb49e3d6b Mon Sep 17 00:00:00 2001 From: Stream Date: Tue, 5 May 2026 15:50:01 +0300 Subject: [PATCH] auto-sync: 2026-05-05 15:50:01 --- .../enduro-trails/DEV_TASK_PHASE5_MINIBAR.md | 230 ++++++++++++++++++ tasks/enduro-trails/prototype/static/app.css | 85 ++----- tasks/enduro-trails/prototype/static/app.js | 103 ++++---- .../enduro-trails/prototype/static/index.html | 14 +- 4 files changed, 322 insertions(+), 110 deletions(-) create mode 100644 tasks/enduro-trails/DEV_TASK_PHASE5_MINIBAR.md diff --git a/tasks/enduro-trails/DEV_TASK_PHASE5_MINIBAR.md b/tasks/enduro-trails/DEV_TASK_PHASE5_MINIBAR.md new file mode 100644 index 0000000..feeef23 --- /dev/null +++ b/tasks/enduro-trails/DEV_TASK_PHASE5_MINIBAR.md @@ -0,0 +1,230 @@ +# Dev Task: Enduro Trails — Мини-бар маршрута + +**Файлы:** `/home/node/.openclaw/workspace/tasks/enduro-trails/prototype/static/` +**Деплой:** `/tmp/deploy_static.js` + +--- + +## Три состояния sheet-route + +| Состояние | Как попасть | Что происходит | +|-----------|-------------|----------------| +| **Полная панель** | Тап «Маршрут» в toolbar | Открыта sheet-route | +| **Мини-бар** | Свайп вниз по handle полной панели | Панель свёрнута, маршрут на карте остаётся | +| **Закрыто** | Крестик (X) в полной панели | Панель закрыта + clearRoute() | + +Мини-бар → тап или свайп вверх → полная панель. +Мини-бар → свайп влево/вправо → переключение вариантов маршрута. + +--- + +## HTML — добавить в index.html перед `` + +```html +
+
+
+
+
+
Вариант 1
+
— км · —% грунт
+
+
+ + +
+
+
+``` + +--- + +## CSS — добавить в app.css + +```css +/* ── Mini Route Bar ───────────────────────── */ +#sheet-route-mini { + position: fixed; + bottom: 72px; left: 0; right: 0; + height: 64px; + background: var(--surface); + border-top: 1px solid var(--border); + border-radius: 14px 14px 0 0; + z-index: 350; + display: none; + flex-direction: column; + align-items: center; + box-shadow: 0 -4px 16px var(--shadow); +} +#sheet-route-mini.visible { display: flex; } +#sheet-route-mini .mini-handle { + width: 32px; height: 4px; + background: var(--border2, var(--border)); + border-radius: 2px; + margin: 7px auto 0; + flex-shrink: 0; +} +.mini-route-info { + display: flex; align-items: center; + gap: 10px; padding: 0 16px; + flex: 1; width: 100%; +} +.mini-route-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; } +.mini-route-text { flex: 1; min-width: 0; } +.mini-route-label { font-size: 13px; font-weight: 700; color: var(--text); } +.mini-route-stats { font-size: 11px; color: var(--text2); } +.mini-route-arrows { display: flex; gap: 4px; flex-shrink: 0; } +.mini-arrow { + width: 28px; height: 28px; + display: flex; align-items: center; justify-content: center; + background: var(--surface2); border: 1px solid var(--border); + border-radius: 8px; font-size: 18px; color: var(--text2); + cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; +} +.mini-arrow:active { background: var(--accent); color: #fff; border-color: var(--accent); } + +@media (min-width: 768px) { + #sheet-route-mini { left: 72px; width: 380px; right: auto; border-radius: 0 14px 0 0; } +} +``` + +--- + +## JS — добавить функции в app.js + +```js +// ─── Mini Route Bar ──────────────────────────────────────────────── +const ROUTE_COLORS = ['#0066ff','#ff6600','#00aa44','#aa00ff','#ff0044']; + +function showMiniRouteSheet() { + if (!routeResults || routeResults.length === 0) return; + updateMiniRouteCard(); + document.getElementById('sheet-route-mini').classList.add('visible'); + initMiniRouteInteraction(); +} + +function hideMiniRouteSheet() { + const el = document.getElementById('sheet-route-mini'); + if (el) el.classList.remove('visible'); +} + +function updateMiniRouteCard() { + const r = routeResults[activeRouteIdx]; + if (!r) return; + const km = (r.distance_m / 1000).toFixed(0); + const dirt = r.stats?.dirt_total_pct ?? '?'; + document.getElementById('mini-dot').style.background = ROUTE_COLORS[activeRouteIdx % ROUTE_COLORS.length]; + document.getElementById('mini-label').textContent = `Вариант ${activeRouteIdx + 1} из ${routeResults.length}`; + document.getElementById('mini-stats').textContent = `${km} км · ${dirt}% грунт`; + document.getElementById('mini-prev').style.opacity = activeRouteIdx > 0 ? '1' : '0.3'; + document.getElementById('mini-next').style.opacity = activeRouteIdx < routeResults.length - 1 ? '1' : '0.3'; +} + +function selectMiniRoute(idx) { + if (idx < 0 || idx >= routeResults.length) return; + activeRouteIdx = idx; + const map = window._map; + for (let i = 0; i < routeResults.length; i++) { + const op = i === idx ? 1 : 0.35; + if (map.getLayer('route-line-' + i)) map.setPaintProperty('route-line-' + i, 'line-opacity', op); + if (map.getLayer('route-line-' + i + '-outline')) map.setPaintProperty('route-line-' + i + '-outline', 'line-opacity', op); + } + updateMiniRouteCard(); +} + +function initMiniRouteInteraction() { + const mini = document.getElementById('sheet-route-mini'); + if (!mini) return; + + // Re-bind arrow buttons + document.getElementById('mini-prev').onclick = (e) => { e.stopPropagation(); selectMiniRoute(activeRouteIdx - 1); }; + document.getElementById('mini-next').onclick = (e) => { e.stopPropagation(); selectMiniRoute(activeRouteIdx + 1); }; + + // Remove old touch listeners by replacing element + const newMini = mini.cloneNode(true); + mini.parentNode.replaceChild(newMini, mini); + document.getElementById('mini-prev').onclick = (e) => { e.stopPropagation(); selectMiniRoute(activeRouteIdx - 1); }; + document.getElementById('mini-next').onclick = (e) => { e.stopPropagation(); selectMiniRoute(activeRouteIdx + 1); }; + + let startX = 0, startY = 0; + newMini.addEventListener('touchstart', e => { + startX = e.touches[0].clientX; + startY = e.touches[0].clientY; + }, { passive: true }); + + newMini.addEventListener('touchend', e => { + const dx = e.changedTouches[0].clientX - startX; + const dy = e.changedTouches[0].clientY - startY; + if (Math.abs(dy) > Math.abs(dx)) { + if (dy < -40) { hideMiniRouteSheet(); openSheet('sheet-route'); } + } else { + if (dx < -40) selectMiniRoute(activeRouteIdx + 1); + if (dx > 40) selectMiniRoute(activeRouteIdx - 1); + } + }); + + newMini.addEventListener('click', e => { + if (e.target.classList.contains('mini-arrow')) return; + hideMiniRouteSheet(); + openSheet('sheet-route'); + }); +} +``` + +--- + +## Изменить существующие функции + +### minimizeSheet(id) +```js +function minimizeSheet(id) { + closeSheet(id); + if (id === 'sheet-route' && routeResults && routeResults.length > 0) { + showMiniRouteSheet(); + } +} +``` + +### clearRoute() — в самое начало добавить: +```js +hideMiniRouteSheet(); +``` + +### toggleRouteMode() — при входе в режим добавить перед openSheet: +```js +hideMiniRouteSheet(); +``` + +### initSheetSwipe() — найти обработчик touchend и изменить логику закрытия: +Найти строку вида `if (dy > 80) closeSheet(sheet.id)` или `if (dy > 80) { closeSheet(...) }` и заменить на: +```js +if (dy > 80) { + if (sheet.id === 'sheet-route' && routeResults && routeResults.length > 0) { + minimizeSheet(sheet.id); + } else { + closeSheet(sheet.id); + } + sheet.style.transform = ''; +} +``` + +### Крестик (X) в sheet-route — уже вызывает minimizeSheet('sheet-route'). +Нужно изменить на toggleRouteMode() чтобы крестик стирал маршрут: +В index.html найти: +```html + @@ -232,7 +232,17 @@
-
+
+
+
+
Вариант 1
+
— км · —% грунт
+
+
+ + +
+