Files
wiki/tasks/enduro-trails/DEV_TASK_PHASE5_MINIBAR.md
2026-05-05 15:50:01 +03:00

8.5 KiB
Raw Blame History

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 перед </body>

<div id="sheet-route-mini">
  <div class="mini-handle" id="mini-route-handle"></div>
  <div class="mini-route-info">
    <div class="mini-route-dot" id="mini-dot"></div>
    <div class="mini-route-text">
      <div class="mini-route-label" id="mini-label">Вариант 1</div>
      <div class="mini-route-stats" id="mini-stats">— км · —% грунт</div>
    </div>
    <div class="mini-route-arrows">
      <span class="mini-arrow" id="mini-prev"></span>
      <span class="mini-arrow" id="mini-next"></span>
    </div>
  </div>
</div>

CSS — добавить в app.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

// ─── 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)

function minimizeSheet(id) {
  closeSheet(id);
  if (id === 'sheet-route' && routeResults && routeResults.length > 0) {
    showMiniRouteSheet();
  }
}

clearRoute() — в самое начало добавить:

hideMiniRouteSheet();

toggleRouteMode() — при входе в режим добавить перед openSheet:

hideMiniRouteSheet();

initSheetSwipe() — найти обработчик touchend и изменить логику закрытия:

Найти строку вида if (dy > 80) closeSheet(sheet.id) или if (dy > 80) { closeSheet(...) } и заменить на:

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 найти:

<button class="sheet-close" onclick="minimizeSheet('sheet-route')">

Заменить на:

<button class="sheet-close" onclick="toggleRouteMode()">

(toggleRouteMode при routeMode=true → выходит из режима + clearRoute)


Проверка после деплоя

  1. Построить маршрут → свайп вниз по handle → появляется мини-бар
  2. Свайп влево/вправо на мини-баре → переключение вариантов (если >1)
  3. Тап на мини-бар → раскрывается полная панель
  4. Крестик в полной панели → всё закрывается, маршрут стирается с карты
  5. Мини-бар не появляется если маршрут не построен