From 773949b3425c1a3189986f6913f0e7fd2db786f7 Mon Sep 17 00:00:00 2001 From: Stream Date: Tue, 5 May 2026 18:10:01 +0300 Subject: [PATCH] auto-sync: 2026-05-05 18:10:01 --- .../enduro-trails/DEV_TASK_PHASE5_MARKERS.md | 289 ++++++++++++++++++ tasks/enduro-trails/prototype/static/app.css | 85 ++++-- tasks/enduro-trails/prototype/static/app.js | 74 ++--- 3 files changed, 392 insertions(+), 56 deletions(-) create mode 100644 tasks/enduro-trails/DEV_TASK_PHASE5_MARKERS.md diff --git a/tasks/enduro-trails/DEV_TASK_PHASE5_MARKERS.md b/tasks/enduro-trails/DEV_TASK_PHASE5_MARKERS.md new file mode 100644 index 0000000..5d85128 --- /dev/null +++ b/tasks/enduro-trails/DEV_TASK_PHASE5_MARKERS.md @@ -0,0 +1,289 @@ +# Dev Task: Enduro Trails — Маркеры и редизайн панели маршрута + +**Файлы:** `/home/node/.openclaw/workspace/tasks/enduro-trails/prototype/static/` +**Деплой:** `node /tmp/deploy_static.js` +**Бэкенд не трогать.** + +--- + +## Часть 1: Маркеры точек на карте + +### Текущее состояние +- Старт: зелёный круг «A» +- Финиш: красный круг «B» +- Промежуточные: белый круг с синей рамкой, цифра + +### Новый дизайн маркеров + +**Старт (index === 0):** флаг-пин зелёный с буквой «S» +**Финиш (index === total-1):** флаг-пин красный с буквой «F» +**Промежуточные:** синий пин с цифрой 1, 2, 3... + +Форма пина (капля/слеза, как у Google Maps): +``` + ╭───╮ + │ S │ + ╰─┬─╯ + │ +``` + +Реализация через SVG в innerHTML маркера: + +```js +function createWaypointMarkerEl(index, total) { + const el = document.createElement('div'); + el.className = 'route-waypoint-marker marker-anim'; + el.style.cssText = 'cursor: grab; width: 28px; height: 36px; position: relative;'; + + let bg, label; + if (index === 0) { + bg = '#2EA043'; label = 'S'; + } else if (index === total - 1) { + bg = '#FF3B1F'; label = 'F'; + } else { + bg = '#0066ff'; label = String(index); + } + + el.innerHTML = ` + + + ${label} + `; + + return el; +} +``` + +CSS для маркера (добавить в app.css): +```css +.route-waypoint-marker { filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4)); } +.route-waypoint-marker:active { cursor: grabbing; } +``` + +--- + +## Часть 2: Список точек в панели — редизайн + +### Текущее состояние +``` +● 55.723, 37.612 [×] +● 56.123, 38.234 [×] +``` + +### Новый дизайн +``` +[S] Москва, Тверская ул. [×] +[1] Клин [×] +[F] Тверь, центр [×] +``` + +Иконка слева — мини-версия пина (SVG, 20×26px), цвет соответствует маркеру на карте. + +```js +function waypointPinSvg(label, color) { + const fs = label.length > 1 ? '7' : '9'; + return ` + + ${label} + `; +} +``` + +Изменить `renderWaypointsList()`: +```js +async function renderWaypointsList() { + const list = document.getElementById('waypoints-list'); + if (!routeWaypoints.length) { list.innerHTML = ''; return; } + + list.innerHTML = routeWaypoints.map((wp, i) => { + const isStart = i === 0; + const isEnd = i === routeWaypoints.length - 1; + const label = isStart ? 'S' : isEnd ? 'F' : String(i); + const color = isStart ? '#2EA043' : isEnd ? '#FF3B1F' : '#0066ff'; + const coordText = `${wp.lat.toFixed(3)}, ${wp.lon.toFixed(3)}`; + return `
+
${waypointPinSvg(label, color)}
+ ${coordText} + +
`; + }).join(''); + + // Async geocode + routeWaypoints.forEach(async (wp, i) => { + const name = await reverseGeocode(wp.lat, wp.lon); + const el = document.getElementById(`wl-label-${i}`); + if (el) el.textContent = name; + }); +} +``` + +CSS для списка точек (обновить): +```css +.wl-item { + display: flex; align-items: center; gap: 8px; + padding: 6px 0; + border-bottom: 1px solid var(--border); +} +.wl-item:last-child { border-bottom: none; } +.wl-pin { flex-shrink: 0; display: flex; align-items: center; } +.wl-label { + flex: 1; font-size: 13px; color: var(--text); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + min-width: 0; +} +.wl-remove { + width: 28px; height: 28px; flex-shrink: 0; + display: flex; align-items: center; justify-content: center; + background: none; border: none; color: var(--text3); + cursor: pointer; border-radius: 6px; + -webkit-tap-highlight-color: transparent; +} +.wl-remove:active { background: var(--red-bg); color: var(--red); } +.wl-remove svg { width: 14px; height: 14px; } +``` + +--- + +## Часть 3: Карточки маршрутов — редизайн + +### Новый дизайн карточки + +``` +┌─────────────────────────────────────┐ +│ ● Вариант 1 1013 км 14ч │ ← header +│ ████████████████░░░░ 82% грунт │ ← полоска + подпись +└─────────────────────────────────────┘ +``` + +- Полоска грунт/асфальт — высота 6px, скруглённая, без отдельных pill-бейджей +- Под полоской: «82% грунт · 18% асфальт» одной строкой мелким текстом +- Активная карточка: оранжевый левый бордер (4px) вместо полного бордера +- Компактнее: padding 10px 12px вместо 12px 14px + +```js +function renderRouteCards(routes) { + const container = document.getElementById('route-cards'); + container.innerHTML = routes.map((route, i) => { + const color = ROUTE_COLORS[i] || '#888888'; + const distKm = (route.distance_m / 1000).toFixed(0); + const timeStr = formatDuration(route.duration_s); + const isActive = i === activeRouteIdx; + const s = route.stats || {}; + const dirtPct = s.dirt_total_pct || 0; + const asphPct = s.asphalt_pct || 0; + + return `
+
+ + Вариант ${i + 1} + ${distKm} км · ${timeStr} +
+
+
+
+
+
+
+
${dirtPct}% грунт${asphPct ? ` · ${asphPct}% асфальт` : ''}
+
`; + }).join(''); +} +``` + +CSS карточек (обновить/добавить): +```css +.route-card { + background: var(--surface2); + border: 1.5px solid var(--border); + border-left: 4px solid transparent; + border-radius: 10px; + padding: 10px 12px; + margin-bottom: 6px; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; +} +.route-card:active { background: var(--surface3, var(--surface2)); } +.route-card.active { + border-color: var(--border); + border-left-color: var(--accent); + background: var(--accent-bg); +} +.rc-header { + display: flex; align-items: center; gap: 8px; + margin-bottom: 8px; +} +.rc-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } +.rc-title { font-size: 13px; font-weight: 700; color: var(--text); flex: 1; } +.rc-meta { font-size: 12px; color: var(--text2); white-space: nowrap; } +.rc-bar-wrap { margin-bottom: 4px; } +.rc-bar { + height: 6px; border-radius: 3px; + background: var(--border); + display: flex; overflow: hidden; +} +.rc-bar-dirt { background: var(--gold); border-radius: 3px 0 0 3px; } +.rc-bar-asphalt { background: var(--text3); } +.rc-bar-label { font-size: 11px; color: var(--text2); } +``` + +--- + +## Часть 4: Кнопки действий — компактнее + +Текущие кнопки «+ Точка», «GPX», «Сброс» — сделать компактнее, иконки без текста на мобиле (или иконка + короткий текст). + +```html + +``` + +CSS: +```css +.route-actions { + display: flex; gap: 6px; margin: 8px 0; +} +.btn-action { + flex: 1; height: 36px; + display: flex; align-items: center; justify-content: center; gap: 5px; + background: var(--surface2); border: 1px solid var(--border); + border-radius: 10px; color: var(--text2); + font-size: 12px; font-weight: 600; cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: background 0.15s, color 0.15s; +} +.btn-action svg { width: 14px; height: 14px; flex-shrink: 0; } +.btn-action:active { background: var(--surface3, var(--border)); } +.btn-action.primary { background: var(--accent); border-color: var(--accent); color: #fff; } +.btn-action.primary:active { opacity: 0.85; } +.btn-action.danger:active { background: var(--red-bg); color: var(--red); border-color: var(--red); } +``` + +--- + +## Порядок реализации +1. Часть 1: новые SVG-маркеры (createWaypointMarkerEl) +2. Часть 2: waypointPinSvg + renderWaypointsList + CSS +3. Часть 3: renderRouteCards + CSS карточек +4. Часть 4: CSS кнопок действий +5. Деплой + проверка + +## Проверка +1. Поставить старт → зелёный пин «S» на карте +2. Поставить финиш → красный пин «F» +3. Добавить промежуточную → синий пин «1» +4. В списке точек: иконки пинов + названия мест +5. Карточки маршрутов: компактные, полоска, левый оранжевый бордер у активной diff --git a/tasks/enduro-trails/prototype/static/app.css b/tasks/enduro-trails/prototype/static/app.css index 8e575d9..acc8a16 100644 --- a/tasks/enduro-trails/prototype/static/app.css +++ b/tasks/enduro-trails/prototype/static/app.css @@ -261,22 +261,47 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; } .wp-add:active { border-color: var(--accent); color: var(--accent); } /* ── Waypoints List ───────────────────────────── */ -#waypoints-list { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; } -.wl-item { display: flex; align-items: center; gap: 8px; background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px; } -.wl-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } -.wl-label { flex: 1; font-size: 13px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.wl-remove { width: 28px; height: 28px; border: none; background: none; color: var(--text3); cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 6px; flex-shrink: 0; } +#waypoints-list { display: flex; flex-direction: column; margin-bottom: 10px; } +.wl-item { + display: flex; align-items: center; gap: 8px; + padding: 6px 0; + border-bottom: 1px solid var(--border); +} +.wl-item:last-child { border-bottom: none; } +.wl-pin { flex-shrink: 0; display: flex; align-items: center; } +.wl-label { + flex: 1; font-size: 13px; color: var(--text); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + min-width: 0; +} +.wl-remove { + width: 28px; height: 28px; flex-shrink: 0; + display: flex; align-items: center; justify-content: center; + background: none; border: none; color: var(--text3); + cursor: pointer; border-radius: 6px; + -webkit-tap-highlight-color: transparent; +} .wl-remove:active { background: var(--red-bg); color: var(--red); } .wl-remove svg { width: 14px; height: 14px; } /* Route actions */ -.route-actions { display: flex; gap: 8px; margin-bottom: 12px; } -.btn-action { flex: 1; height: 38px; background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; color: var(--text2); font-size: 12px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 5px; cursor: pointer; transition: all 0.15s; } -.btn-action svg { width: 14px; height: 14px; } -.btn-action:active { background: var(--surface3); transform: scale(0.94); } +.route-actions { + display: flex; gap: 6px; margin: 8px 0; +} +.btn-action { + flex: 1; height: 36px; + display: flex; align-items: center; justify-content: center; gap: 5px; + background: var(--surface2); border: 1px solid var(--border); + border-radius: 10px; color: var(--text2); + font-size: 12px; font-weight: 600; cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: background 0.15s, color 0.15s; +} +.btn-action svg { width: 14px; height: 14px; flex-shrink: 0; } +.btn-action:active { background: var(--surface3, var(--border)); } +.btn-action.primary { background: var(--accent); border-color: var(--accent); color: #fff; } +.btn-action.primary:active { opacity: 0.85; } .btn-action.danger:active { background: var(--red-bg); color: var(--red); border-color: var(--red); } -.btn-action.primary { border-color: var(--accent); color: var(--accent); } -.btn-action.primary:active { background: var(--accent-bg); } /* ── Route Status ─────────────────────────────── */ #route-status { font-size: 13px; color: var(--text2); padding: 8px 0; display: flex; align-items: center; gap: 6px; } @@ -284,21 +309,36 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; } /* ── Route Cards ──────────────────────────────── */ #route-cards, #link-cards, #scenic-cards { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; } .route-card { - background: var(--surface2); border: 1.5px solid var(--border); - border-radius: 14px; padding: 12px 14px; cursor: pointer; - transition: border-color 0.15s, box-shadow 0.15s, opacity 0.2s; + background: var(--surface2); + border: 1.5px solid var(--border); + border-left: 4px solid transparent; + border-radius: 10px; + padding: 10px 12px; + margin-bottom: 0; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; -webkit-tap-highlight-color: transparent; animation: cardFadeIn 0.2s ease-out both; } -.route-card.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); } +.route-card:active { background: var(--surface3, var(--surface2)); } +.route-card.active { + border-color: var(--border); + border-left-color: var(--accent); + background: var(--accent-bg); +} .rc-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } .rc-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } -.rc-title { flex: 1; font-size: 13px; font-weight: 700; color: var(--text); } -.rc-km { font-size: 14px; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; } -.rc-time { font-size: 12px; color: var(--text2); font-variant-numeric: tabular-nums; } -.rc-bar { height: 5px; border-radius: 3px; background: var(--surface3); overflow: hidden; margin-bottom: 8px; display: flex; } -.rc-bar-dirt { background: var(--gold); height: 100%; transition: width 0.4s; } -.rc-bar-asphalt { background: var(--text3); height: 100%; flex: 1; } +.rc-title { font-size: 13px; font-weight: 700; color: var(--text); flex: 1; } +.rc-meta { font-size: 12px; color: var(--text2); white-space: nowrap; font-variant-numeric: tabular-nums; } +.rc-bar-wrap { margin-bottom: 4px; } +.rc-bar { + height: 6px; border-radius: 3px; + background: var(--border); + display: flex; overflow: hidden; +} +.rc-bar-dirt { background: var(--gold); border-radius: 3px 0 0 3px; transition: width 0.4s; } +.rc-bar-asphalt { background: var(--text3); } +.rc-bar-label { font-size: 11px; color: var(--text2); } .rc-stats { display: flex; flex-wrap: wrap; gap: 5px; } /* Stat pills */ @@ -411,7 +451,8 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; } #ruler-info.visible { display: flex; align-items: center; gap: 8px; } /* ── Waypoint Markers ─────────────────────────── */ -.route-waypoint-marker { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 800; color: #fff; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.5); border: 2px solid rgba(255,255,255,0.8); } +.route-waypoint-marker { filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4)); } +.route-waypoint-marker:active { cursor: grabbing; } .named-marker-el { font-size: 22px; cursor: pointer; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5)); user-select: none; line-height: 1; } /* ═══════════════════════════════════════════════════ diff --git a/tasks/enduro-trails/prototype/static/app.js b/tasks/enduro-trails/prototype/static/app.js index b7836bd..4261705 100644 --- a/tasks/enduro-trails/prototype/static/app.js +++ b/tasks/enduro-trails/prototype/static/app.js @@ -460,19 +460,22 @@ function addWaypointMode() { function createWaypointMarkerEl(index, total) { const el = document.createElement('div'); el.className = 'route-waypoint-marker marker-anim'; - let bg, text, color = '#fff'; + el.style.cssText = 'cursor: grab; width: 28px; height: 36px; position: relative;'; + + let bg, label; if (index === 0) { - bg = '#2EA043'; text = 'A'; + bg = '#2EA043'; label = 'S'; } else if (index === total - 1) { - bg = '#FF3B1F'; text = 'B'; + bg = '#FF3B1F'; label = 'F'; } else { - bg = '#fff'; text = String(index); color = '#0066ff'; - el.style.cssText = `width:18px;height:18px;background:${bg};border:2px solid #0066ff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;color:${color};box-shadow:0 1px 4px rgba(0,0,0,0.3);cursor:grab;`; - el.textContent = text; - return el; + bg = '#0066ff'; label = String(index); } - el.style.cssText = `width:22px;height:22px;background:${bg};border:2px solid #fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:${color};box-shadow:0 1px 4px rgba(0,0,0,0.3);cursor:grab;`; - el.textContent = text; + + el.innerHTML = ` + + ${label} + `; + return el; } @@ -518,25 +521,34 @@ async function reverseGeocode(lat, lon) { } } +function waypointPinSvg(label, color) { + const fs = label.length > 1 ? '7' : '9'; + return ` + + ${label} + `; +} + async function renderWaypointsList() { const list = document.getElementById('waypoints-list'); if (!routeWaypoints.length) { list.innerHTML = ''; return; } - // Render immediately with coords, then update with place names list.innerHTML = routeWaypoints.map((wp, i) => { - const labelClass = i === 0 ? 'start' : i === routeWaypoints.length - 1 ? 'end' : 'mid'; - const color = labelClass === 'start' ? 'var(--success)' : labelClass === 'end' ? 'var(--red)' : '#0066ff'; + const isStart = i === 0; + const isEnd = i === routeWaypoints.length - 1; + const label = isStart ? 'S' : isEnd ? 'F' : String(i); + const color = isStart ? '#2EA043' : isEnd ? '#FF3B1F' : '#0066ff'; const coordText = `${wp.lat.toFixed(3)}, ${wp.lon.toFixed(3)}`; return `
-
+
${waypointPinSvg(label, color)}
${coordText}
`; }).join(''); - // Async update labels with place names + // Async geocode routeWaypoints.forEach(async (wp, i) => { const name = await reverseGeocode(wp.lat, wp.lon); const el = document.getElementById(`wl-label-${i}`); @@ -696,32 +708,26 @@ function renderRouteCards(routes) { const container = document.getElementById('route-cards'); container.innerHTML = routes.map((route, i) => { const color = ROUTE_COLORS[i] || '#888888'; - const distKm = (route.distance_m / 1000).toFixed(1); + const distKm = (route.distance_m / 1000).toFixed(0); const timeStr = formatDuration(route.duration_s); const isActive = i === activeRouteIdx; - - let statsHtml = ''; - if (route.stats) { - const s = route.stats; - statsHtml = ` -
-
-
-
-
- 🟡 ${s.dirt_total_pct || 0}% грунт - ${s.asphalt_pct ? `⬜ ${s.asphalt_pct}% асфальт` : ''} -
`; - } - + const s = route.stats || {}; + const dirtPct = s.dirt_total_pct || 0; + const asphPct = s.asphalt_pct || 0; + return `
Вариант ${i + 1} - ${distKm} км - ${timeStr} + ${distKm} км · ${timeStr}
- ${statsHtml} +
+
+
+
+
+
+
${dirtPct}% грунт${asphPct ? ` · ${asphPct}% асфальт` : ''}
`; }).join(''); }