auto-sync: 2026-05-05 15:50:01

This commit is contained in:
Stream
2026-05-05 15:50:01 +03:00
parent 665baab132
commit 15b4aace79
4 changed files with 322 additions and 110 deletions

View File

@@ -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 перед `</body>`
```html
<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
```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
<button class="sheet-close" onclick="minimizeSheet('sheet-route')">
```
Заменить на:
```html
<button class="sheet-close" onclick="toggleRouteMode()">
```
(toggleRouteMode при routeMode=true → выходит из режима + clearRoute)
---
## Проверка после деплоя
1. Построить маршрут → свайп вниз по handle → появляется мини-бар
2. Свайп влево/вправо на мини-баре → переключение вариантов (если >1)
3. Тап на мини-бар → раскрывается полная панель
4. Крестик в полной панели → всё закрывается, маршрут стирается с карты
5. Мини-бар не появляется если маршрут не построен

View File

@@ -553,78 +553,47 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
.search-result-name { font-size: 14px; font-weight: 500; color: var(--text); }
.search-result-detail { font-size: 12px; color: var(--text2); margin-top: 2px; }
/* ── Mini route sheet ─────────────────────────────────────────── */
/* ── Mini Route Bar ───────────────────────── */
#sheet-route-mini {
position: fixed;
bottom: 72px; /* above toolbar */
left: 0; right: 0;
height: 80px;
bottom: 72px; left: 0; right: 0;
height: 64px;
background: var(--surface);
border-top: 1px solid var(--border);
border-radius: 16px 16px 0 0;
border-radius: 14px 14px 0 0;
z-index: 350;
display: none;
flex-direction: column;
box-shadow: 0 -4px 20px var(--shadow);
align-items: center;
box-shadow: 0 -4px 16px var(--shadow);
}
#sheet-route-mini.visible { display: flex; }
.mini-handle {
width: 36px; height: 4px;
background: var(--border);
#sheet-route-mini .mini-handle {
width: 32px; height: 4px;
background: var(--border2, var(--border));
border-radius: 2px;
margin: 8px auto 4px;
flex-shrink: 0;
cursor: pointer;
}
.mini-cards-row {
display: flex;
gap: 8px;
overflow-x: auto;
padding: 0 12px 10px;
scrollbar-width: none;
flex: 1;
align-items: center;
}
.mini-cards-row::-webkit-scrollbar { display: none; }
.mini-route-card {
display: flex;
align-items: center;
gap: 8px;
background: var(--surface2);
border: 1.5px solid var(--border);
border-radius: 10px;
padding: 6px 12px;
flex-shrink: 0;
cursor: pointer;
transition: border-color 0.15s;
white-space: nowrap;
}
.mini-route-card.active {
border-color: var(--accent);
background: var(--accent-bg);
}
.mini-route-dot {
width: 10px; height: 10px;
border-radius: 50%;
margin: 7px auto 0;
flex-shrink: 0;
}
.mini-route-label {
font-size: 13px; font-weight: 600;
color: var(--text);
.mini-route-info {
display: flex; align-items: center;
gap: 10px; padding: 0 16px;
flex: 1; width: 100%;
}
.mini-route-stats {
font-size: 11px;
color: var(--text2);
.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 16px 0 0;
}
#sheet-route-mini { left: 72px; width: 380px; right: auto; border-radius: 0 14px 0 0; }
}

View File

@@ -1612,75 +1612,78 @@ document.addEventListener('DOMContentLoaded', () => {
applyTheme();
});
// ─── Mini route sheet ──────────────────────────────────────────────
// ─── Mini Route Bar ──────────────────────────────────────────────────
const MINI_ROUTE_COLORS = ['#0066ff','#ff6600','#00aa44','#aa00ff','#ff0044'];
function showMiniRouteSheet() {
if (routeResults.length === 0) return;
const mini = document.getElementById('sheet-route-mini');
const row = document.getElementById('mini-route-cards');
row.innerHTML = routeResults.map((r, i) => {
const km = (r.distance_m / 1000).toFixed(0);
const dirt = r.stats?.dirt_total_pct ?? '?';
const color = ROUTE_COLORS[i] || '#888888';
const active = i === activeRouteIdx ? 'active' : '';
return `<div class="mini-route-card ${active}" onclick="selectRouteFromMini(${i})">
<div class="mini-route-dot" style="background:${color}"></div>
<div>
<div class="mini-route-label">Вариант ${i + 1}</div>
<div class="mini-route-stats">${km} км · ${dirt}% грунт</div>
</div>
</div>`;
}).join('');
mini.classList.add('visible');
initMiniSheetSwipe();
if (!routeResults || routeResults.length === 0) return;
updateMiniRouteCard();
document.getElementById('sheet-route-mini').classList.add('visible');
initMiniRouteInteraction();
}
function hideMiniRouteSheet() {
const mini = document.getElementById('sheet-route-mini');
if (mini) mini.classList.remove('visible');
const el = document.getElementById('sheet-route-mini');
if (el) el.classList.remove('visible');
}
function selectRouteFromMini(idx) {
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 = MINI_ROUTE_COLORS[activeRouteIdx % MINI_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 isActive = i === idx;
const op = i === idx ? 1 : 0.35;
try {
if (map.getLayer('route-line-' + i)) {
map.setPaintProperty('route-line-' + i, 'line-width', isActive ? 5 : 3);
map.setPaintProperty('route-line-' + i, 'line-opacity', isActive ? 0.95 : 0.5);
}
if (map.getLayer('route-line-' + i + '-outline')) {
map.setPaintProperty('route-line-' + i + '-outline', 'line-width', isActive ? 7 : 4);
map.setPaintProperty('route-line-' + i + '-outline', 'line-opacity', isActive ? 0.6 : 0);
}
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', i === idx ? 0.6 : 0);
} catch(e) {}
}
// Update mini cards active state
document.querySelectorAll('.mini-route-card').forEach((c, i) => {
c.classList.toggle('active', i === idx);
});
updateMiniRouteCard();
}
function initMiniSheetSwipe() {
const handle = document.getElementById('mini-route-handle');
if (!handle) return;
// Remove old listeners by cloning
const newHandle = handle.cloneNode(true);
handle.parentNode.replaceChild(newHandle, handle);
function initMiniRouteInteraction() {
const mini = document.getElementById('sheet-route-mini');
if (!mini) return;
let startY = 0;
newHandle.addEventListener('touchstart', e => { startY = e.touches[0].clientY; }, { passive: true });
newHandle.addEventListener('touchend', e => {
// Replace element to drop all old listeners
const newMini = mini.cloneNode(true);
mini.parentNode.replaceChild(newMini, mini);
// 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); };
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 (dy < -40) {
// Swipe up → expand full sheet
hideMiniRouteSheet();
openSheet('sheet-route');
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);
}
});
// Tap on handle = expand
newHandle.addEventListener('click', () => {
newMini.addEventListener('click', e => {
if (e.target.classList.contains('mini-arrow')) return;
hideMiniRouteSheet();
openSheet('sheet-route');
});

View File

@@ -60,7 +60,7 @@
<div class="sheet-header">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z"/><path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/><path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z"/><path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z"/><path d="M14 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-3"/></svg>
<h2>Маршрут</h2>
<button class="sheet-close" onclick="minimizeSheet('sheet-route')">
<button class="sheet-close" onclick="toggleRouteMode()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
</div>
@@ -232,7 +232,17 @@
<!-- Mini route sheet -->
<div id="sheet-route-mini">
<div class="mini-handle" id="mini-route-handle"></div>
<div class="mini-cards-row" id="mini-route-cards"></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>
<!-- Scripts -->